From c3b109174d3e3d254e04bcae94f8ecb511d4106b Mon Sep 17 00:00:00 2001 From: Sam Day Date: Sun, 15 Mar 2026 11:07:30 +1000 Subject: [PATCH 01/16] Squashed 'phosh/' content from commit 62fde093 git-subtree-dir: phosh git-subtree-split: 62fde093044d90b82154e68a39feb8e6ea967ef5 --- .dir-locals.el | 22 + .editorconfig | 29 + .gitattributes | 1 + .github/FUNDING.yml | 2 + .gitignore | 38 + .gitlab-ci.yml | 430 + .gitlab-ci/Makefile | 12 + .gitlab-ci/README.md | 12 + .gitlab-ci/asan.Dockerfile | 10 + .gitlab-ci/check-consistency | 102 + .gitlab-ci/check-meson | 39 + .gitlab-ci/check-po | 25 + .gitlab-ci/check-style.py | 185 + .gitlab-ci/check-ui | 72 + .gitlab-ci/commit-rules.yml | 20 + .gitlab-ci/debian.Dockerfile | 17 + .gitlab-ci/g-style | 32 + .gitlab-ci/libphosh-test.c | 13 + .gitlab-ci/run-docker.sh | 139 + .gitlab-ci/screenshot.Dockerfile | 15 + .gitlab-ci/uncrustify.cfg | 148 + .gitlab/issue_templates/bug.md | 93 + .gitmodules | 0 .ruff.toml | 1 + COPYING | 674 + HACKING.md | 595 + NEWS | 1479 ++ README.md | 130 + calendar-server/CalendarServer.service.in | 3 + calendar-server/calendar-debug.h | 39 + calendar-server/calendar-server.c | 1102 ++ calendar-server/calendar-sources.c | 495 + calendar-server/calendar-sources.h | 53 + calendar-server/meson.build | 37 + data/00_mobi.Phosh.gschema.override | 37 + data/icons/app-close-symbolic.svg | 4 + data/icons/app-icon-unknown-symbolic.svg | 6 + data/icons/app-icon-unknown.svg | 19 + data/icons/asterisk-symbolic.svg | 1 + data/icons/audio-handsfree-symbolic.svg | 4 + data/icons/auth-sim-locked-symbolic.svg | 36 + data/icons/auth-sim-missing-symbolic.svg | 36 + data/icons/auto-brightness-symbolic.svg | 2 + .../camera-hardware-disabled-symbolic.svg | 6 + data/icons/chat-none-symbolic.svg | 2 + data/icons/chat-symbolic.svg | 2 + data/icons/eye-not-looking-symbolic.svg | 3 + .../eye-open-negative-filled-symbolic.svg | 26 + data/icons/feedback-quiet-symbolic.svg | 31 + data/icons/input-powerbar-symbolic.svg | 1 + data/icons/meson.build | 4 + .../microphone-hardware-disabled-symbolic.svg | 6 + data/icons/mobi.phosh.Shell-symbolic.svg | 5 + data/icons/mobile-data-disabled-symbolic.svg | 1 + data/icons/mobile-data-symbolic.svg | 1 + data/icons/moon-filled-symbolic.svg | 4 + .../network-cellular-disabled-symbolic.svg | 66 + ...ular-no-data-signal-excellent-symbolic.svg | 5 + ...-cellular-no-data-signal-good-symbolic.svg | 6 + ...-cellular-no-data-signal-none-symbolic.svg | 5 + ...rk-cellular-no-data-signal-ok-symbolic.svg | 6 + ...-cellular-no-data-signal-weak-symbolic.svg | 6 + data/icons/network-vpn-disabled-symbolic.svg | 19 + .../network-wireless-disabled-symbolic.svg | 6 + data/icons/no-notifications-symbolic.svg | 4 + data/icons/padlock-symbolic.svg | 1 + data/icons/phone-docked-symbolic.svg | 50 + data/icons/phone-undocked-symbolic.svg | 31 + .../screen-rotation-landscape-symbolic.svg | 68 + .../screen-rotation-portrait-symbolic.svg | 68 + data/icons/screenshot-portrait-symbolic.svg | 1 + data/icons/settings-symbolic.svg | 2 + data/icons/skip-backwards-10-symbolic.svg | 4 + data/icons/skip-forward-30-symbolic.svg | 8 + .../icons/splash-process-working-symbolic.svg | 4 + data/icons/swipe-arrow-symbolic.svg | 35 + data/icons/torch-disabled-symbolic.svg | 7 + data/icons/torch-enabled-symbolic.svg | 6 + data/leak-suppress.txt | 30 + data/meson.build | 154 + data/mobi.phosh.Shell.Search.service.in | 3 + data/mobi.phosh.Shell.desktop.in.in | 15 + data/mobi.phosh.shell.gschema.xml | 212 + data/phoc.ini | 26 + data/phosh-portals.conf | 12 + data/phosh-session.in | 55 + data/phosh-shell.portal | 4 + data/phosh.service | 62 + data/phosh.session.desktop.in.in | 4 + data/systemd/meson.build | 34 + data/systemd/mobi.phosh.OSK.target | 11 + data/systemd/mobi.phosh.Shell.service.in | 27 + data/systemd/mobi.phosh.Shell.target | 10 + data/systemd/phosh.session.conf.in | 6 + data/valgrind.suppressions | 62 + data/wayland-sessions/phosh.desktop | 7 + debian/README.source | 29 + debian/changelog | 12885 ++++++++++++++++ debian/clean | 4 + debian/control | 247 + debian/copyright | 593 + debian/gbp.conf | 11 + debian/gir1.2-phosh-0-dev.install | 1 + debian/libphosh-0.45-0.install | 1 + debian/libphosh-0.45-0.symbols | 134 + debian/libphosh-0.45-dev.install | 3 + debian/not-installed | 3 + debian/phosh-common.install | 3 + debian/phosh-dev.install | 2 + debian/phosh-doc.install | 1 + debian/phosh-doc.links | 1 + debian/phosh-mobile-tweaks.install | 1 + debian/phosh-plugins.install | 6 + debian/phosh.dirs | 3 + debian/phosh.docs | 1 + debian/phosh.install | 17 + debian/phosh.manpages | 3 + debian/phosh.postinst.in | 14 + debian/phosh.postrm.in | 13 + debian/phosh.triggers.in | 1 + debian/rules | 47 + debian/source/format | 1 + debian/tests/control | 7 + docs/app-dev.md | 48 + docs/gettingstarted.md | 192 + docs/meson.build | 77 + docs/phosh-dbus-sm.puri.OSK0.md | 39 + docs/phosh-session.rst | 56 + docs/phosh.gsettings.rst | 144 + docs/phosh.rst | 94 + docs/phosh.toml.in | 56 + docs/urlmap.js | 9 + gcovr.cfg | 21 + meson.build | 296 + meson_options.txt | 61 + phosh.doap | 33 + .../caffeine-quick-setting.c | 412 + .../caffeine-quick-setting.desktop.in.in | 12 + .../caffeine-quick-setting.h | 21 + .../icons/cafe-cold-symbolic.svg | 137 + .../icons/cafe-hot-symbolic.svg | 2 + plugins/caffeine-quick-setting/interval-row.c | 174 + plugins/caffeine-quick-setting/interval-row.h | 23 + .../caffeine-quick-setting/interval-row.ui | 27 + plugins/caffeine-quick-setting/meson.build | 68 + ...plugins.caffeine-quick-setting.gschema.xml | 22 + .../phosh-plugin-caffeine-quick-setting.c | 40 + ...ugin-caffeine-quick-setting.gresources.xml | 9 + .../prefs/caffeine-quick-setting-prefs.c | 306 + .../prefs/caffeine-quick-setting-prefs.h | 19 + .../caffeine-quick-setting/prefs/meson.build | 25 + ...hosh-plugin-prefs-caffeine-quick-setting.c | 41 + ...refs-caffeine-quick-setting.gresources.xml | 7 + plugins/caffeine-quick-setting/prefs/prefs.ui | 292 + .../prefs/stylesheet/common.css | 11 + plugins/caffeine-quick-setting/qs.ui | 59 + plugins/calendar/calendar.c | 95 + plugins/calendar/calendar.desktop.in.in | 8 + plugins/calendar/calendar.h | 19 + plugins/calendar/calendar.ui | 16 + plugins/calendar/meson.build | 39 + plugins/calendar/phosh-plugin-calendar.c | 39 + .../phosh-plugin-calendar.gresources.xml | 7 + plugins/calendar/stylesheet/common.css | 5 + .../dark-mode-quick-setting.c | 131 + .../dark-mode-quick-setting.desktop.in.in | 8 + .../dark-mode-quick-setting.h | 21 + .../icons/dark-mode-disabled-symbolic.svg | 1 + .../icons/dark-mode-symbolic.svg | 4 + plugins/dark-mode-quick-setting/meson.build | 52 + .../phosh-plugin-dark-mode-quick-setting.c | 40 + ...gin-dark-mode-quick-setting.gresources.xml | 8 + plugins/dark-mode-quick-setting/qs.ui | 12 + .../emergency-info/emergency-info-common.h | 12 + plugins/emergency-info/emergency-info-row.c | 113 + plugins/emergency-info/emergency-info-row.h | 24 + plugins/emergency-info/emergency-info-row.ui | 15 + plugins/emergency-info/emergency-info.c | 328 + .../emergency-info.desktop.in.in | 11 + plugins/emergency-info/emergency-info.h | 20 + plugins/emergency-info/emergency-info.ui | 221 + plugins/emergency-info/meson.build | 51 + .../phosh-plugin-emergency-info.c | 38 + ...phosh-plugin-emergency-info.gresources.xml | 8 + .../prefs/emergency-info-prefs-row.c | 138 + .../prefs/emergency-info-prefs-row.h | 22 + .../prefs/emergency-info-prefs-row.ui | 23 + .../prefs/emergency-info-prefs.c | 483 + .../prefs/emergency-info-prefs.h | 16 + .../prefs/emergency-info-prefs.ui | 263 + plugins/emergency-info/prefs/meson.build | 32 + .../prefs/phosh-plugin-prefs-emergency-info.c | 42 + ...plugin-prefs-emergency-info.gresources.xml | 7 + plugins/emergency-info/stylesheet/common.css | 5 + plugins/launcher-box/launcher-box.c | 302 + .../launcher-box/launcher-box.desktop.in.in | 6 + plugins/launcher-box/launcher-box.h | 19 + plugins/launcher-box/launcher-box.ui | 52 + plugins/launcher-box/launcher-item.c | 276 + plugins/launcher-box/launcher-item.h | 29 + plugins/launcher-box/launcher-row.c | 191 + plugins/launcher-box/launcher-row.h | 25 + plugins/launcher-box/launcher-row.ui | 33 + plugins/launcher-box/meson.build | 59 + .../launcher-box/phosh-plugin-launcher-box.c | 38 + .../phosh-plugin-launcher-box.gresources.xml | 8 + ...uri.phosh.plugins.launcher-box.gschema.xml | 13 + plugins/launcher-box/stylesheet/common.css | 12 + .../icons/location-quick-setting-symbolic.svg | 4 + .../location-quick-setting.c | 120 + .../location-quick-setting.desktop.in.in | 9 + .../location-quick-setting.h | 20 + plugins/location-quick-setting/meson.build | 47 + .../phosh-plugin-location-quick-setting.c | 39 + ...ugin-location-quick-setting.gresources.xml | 6 + plugins/location-quick-setting/qs.ui | 12 + plugins/media-players/media-players.c | 118 + .../media-players/media-players.desktop.in.in | 6 + plugins/media-players/media-players.h | 19 + plugins/media-players/media-players.ui | 55 + plugins/media-players/meson.build | 46 + .../phosh-plugin-media-players.c | 38 + .../phosh-plugin-media-players.gresources.xml | 7 + plugins/media-players/stylesheet/common.css | 0 plugins/meson.build | 126 + .../mobile-data-quick-setting-symbolic.svg | 1 + plugins/mobile-data-quick-setting/meson.build | 48 + .../mobile-data-quick-setting.c | 135 + .../mobile-data-quick-setting.desktop.in.in | 8 + .../mobile-data-quick-setting.h | 21 + .../phosh-plugin-mobile-data-quick-setting.c | 39 + ...n-mobile-data-quick-setting.gresources.xml | 6 + plugins/mobile-data-quick-setting/qs.ui | 12 + .../night-light-quick-setting-symbolic.svg | 14 + plugins/night-light-quick-setting/meson.build | 41 + .../night-light-quick-setting.c | 125 + .../night-light-quick-setting.desktop.in.in | 8 + .../night-light-quick-setting.h | 21 + .../phosh-plugin-night-light-quick-setting.c | 40 + ...n-night-light-quick-setting.gresources.xml | 6 + plugins/night-light-quick-setting/qs.ui | 12 + plugins/phosh-plugin.h.in | 14 + .../icons/pomodoro-active-symbolic.svg | 2 + .../icons/pomodoro-break-symbolic.svg | 2 + .../icons/pomodoro-off-symbolic.svg | 1 + plugins/pomodoro-quick-setting/meson.build | 74 + .../mobi.phosh.plugins.pomodoro.gschema.xml | 26 + .../phosh-plugin-pomodoro-quick-setting.c | 40 + ...ugin-pomodoro-quick-setting.gresources.xml | 9 + .../pomodoro-quick-setting.c | 390 + .../pomodoro-quick-setting.desktop.in.in | 13 + .../pomodoro-quick-setting.h | 25 + .../pomodoro-quick-setting/prefs/meson.build | 25 + ...hosh-plugin-prefs-pomodoro-quick-setting.c | 43 + ...refs-pomodoro-quick-setting.gresources.xml | 6 + .../prefs/pomodoro-quick-setting-prefs.c | 122 + .../prefs/pomodoro-quick-setting-prefs.h | 18 + plugins/pomodoro-quick-setting/prefs/prefs.ui | 54 + plugins/pomodoro-quick-setting/qs.ui | 12 + .../icons/screen-scaling-large-symbolic.svg | 1 + .../icons/screen-scaling-small-symbolic.svg | 1 + plugins/scaling-quick-setting/meson.build | 49 + .../phosh-plugin-scaling-quick-setting.c | 40 + ...lugin-scaling-quick-setting.gresources.xml | 9 + plugins/scaling-quick-setting/qs.ui | 25 + plugins/scaling-quick-setting/scale-row.c | 172 + plugins/scaling-quick-setting/scale-row.h | 20 + plugins/scaling-quick-setting/scale-row.ui | 27 + .../scaling-quick-setting.c | 226 + .../scaling-quick-setting.desktop.in.in | 8 + .../scaling-quick-setting.h | 20 + .../simple-custom-quick-setting/meson.build | 42 + ...phosh-plugin-simple-custom-quick-setting.c | 40 + ...simple-custom-quick-setting.gresources.xml | 6 + plugins/simple-custom-quick-setting/qs.ui | 40 + .../simple-custom-quick-setting.c | 72 + .../simple-custom-quick-setting.desktop.in.in | 8 + .../simple-custom-quick-setting.h | 21 + plugins/ticket-box/meson.build | 69 + plugins/ticket-box/phosh-plugin-ticket-box.c | 38 + .../phosh-plugin-ticket-box.gresources.xml | 7 + plugins/ticket-box/prefs/meson.build | 25 + .../prefs/phosh-plugin-prefs-ticket-box.c | 43 + ...osh-plugin-prefs-ticket-box.gresources.xml | 6 + plugins/ticket-box/prefs/ticket-box-prefs.c | 148 + plugins/ticket-box/prefs/ticket-box-prefs.h | 16 + plugins/ticket-box/prefs/ticket-box-prefs.ui | 44 + ....puri.phosh.plugins.ticket-box.gschema.xml | 13 + plugins/ticket-box/stylesheet/common.css | 9 + plugins/ticket-box/ticket-box.c | 249 + plugins/ticket-box/ticket-box.desktop.in.in | 10 + plugins/ticket-box/ticket-box.h | 19 + plugins/ticket-box/ticket-box.ui | 101 + plugins/ticket-box/ticket-row.c | 116 + plugins/ticket-box/ticket-row.h | 22 + plugins/ticket-box/ticket.c | 155 + plugins/ticket-box/ticket.h | 23 + plugins/upcoming-events/calendar-event.c | 222 + plugins/upcoming-events/calendar-event.h | 28 + plugins/upcoming-events/event-list.c | 385 + plugins/upcoming-events/event-list.h | 21 + plugins/upcoming-events/event-list.ui | 52 + .../icons/upcoming-events-all-symbolic.svg | 1 + .../upcoming-events-skip-empty-symbolic.svg | 1 + plugins/upcoming-events/meson.build | 74 + .../mobi.phosh.Shell.CalendarServer.xml | 19 + .../phosh-plugin-upcoming-events.c | 38 + ...hosh-plugin-upcoming-events.gresources.xml | 11 + plugins/upcoming-events/prefs/meson.build | 25 + .../phosh-plugin-prefs-upcoming-events.c | 40 + ...lugin-prefs-upcoming-events.gresources.xml | 6 + .../prefs/upcoming-events-prefs.c | 66 + .../prefs/upcoming-events-prefs.h | 16 + .../prefs/upcoming-events-prefs.ui | 32 + ....phosh.plugins.upcoming-events.gschema.xml | 16 + plugins/upcoming-events/stylesheet/common.css | 35 + plugins/upcoming-events/upcoming-event.c | 401 + plugins/upcoming-events/upcoming-event.h | 26 + plugins/upcoming-events/upcoming-event.ui | 43 + plugins/upcoming-events/upcoming-events.c | 526 + .../upcoming-events.desktop.in.in | 10 + plugins/upcoming-events/upcoming-events.h | 19 + plugins/upcoming-events/upcoming-events.ui | 58 + ...rk-wireless-hotspot-acquiring-symbolic.svg | 1 + ...ork-wireless-hotspot-disabled-symbolic.svg | 1 + .../wifi-hotspot-quick-setting-symbolic.svg | 5 + .../wifi-hotspot-quick-setting/meson.build | 44 + .../phosh-plugin-wifi-hotspot-quick-setting.c | 40 + ...-wifi-hotspot-quick-setting.gresources.xml | 10 + plugins/wifi-hotspot-quick-setting/qs.ui | 14 + .../wifi-hotspot-quick-setting/status-page.ui | 89 + plugins/wifi-hotspot-quick-setting/style.css | 5 + .../wifi-hotspot-quick-setting.c | 166 + .../wifi-hotspot-quick-setting.desktop.in.in | 8 + .../wifi-hotspot-quick-setting.h | 21 + .../wifi-hotspot-status-page.c | 421 + .../wifi-hotspot-status-page.h | 20 + po/LINGUAS | 51 + po/POTFILES.in | 146 + po/POTFILES.skip | 7 + po/am.po | 145 + po/ar.po | 146 + po/be.po | 1178 ++ po/bg.po | 1249 ++ po/ca.po | 1326 ++ po/cs.po | 1251 ++ po/da.po | 405 + po/de.po | 1343 ++ po/el.po | 1306 ++ po/en_GB.po | 1195 ++ po/eo.po | 1181 ++ po/es.po | 758 + po/eu.po | 1299 ++ po/fa.po | 1239 ++ po/fi.po | 1332 ++ po/fr.po | 1111 ++ po/fur.po | 430 + po/fy.po | 772 + po/gl.po | 782 + po/he.po | 1338 ++ po/hi.po | 951 ++ po/hr.po | 643 + po/ht.po | 1128 ++ po/hu.po | 971 ++ po/id.po | 1083 ++ po/it.po | 835 + po/ja.po | 403 + po/ka.po | 1322 ++ po/ko.po | 288 + po/kw.po | 1243 ++ po/la.po | 146 + po/lv.po | 327 + po/meson.build | 2 + po/nb.po | 547 + po/nl.po | 1228 ++ po/oc.po | 1331 ++ po/pl.po | 773 + po/pt.po | 1118 ++ po/pt_BR.po | 1334 ++ po/ro.po | 1249 ++ po/ru.po | 1336 ++ po/sat.po | 1319 ++ po/sk.po | 439 + po/sl.po | 1257 ++ po/sr.po | 713 + po/sv.po | 1323 ++ po/tr.po | 1226 ++ po/uk.po | 1340 ++ po/uz.po | 1100 ++ po/zh_CN.po | 797 + po/zh_Hans_CN.po | 145 + po/zh_TW.po | 171 + protocol/input-method-unstable-v2.xml | 490 + protocol/meson.build | 48 + protocol/phoc-device-state-unstable-v1.xml | 153 + .../phoc-layer-shell-effects-unstable-v1.xml | 288 + protocol/phosh-private.xml | 240 + protocol/virtual-keyboard-unstable-v1.xml | 113 + ...oreign-toplevel-management-unstable-v1.xml | 270 + protocol/wlr-gamma-control-unstable-v1.xml | 126 + protocol/wlr-layer-shell-unstable-v1.xml | 390 + .../wlr-output-management-unstable-v1.xml | 554 + ...lr-output-power-management-unstable-v1.xml | 128 + protocol/wlr-screencopy-unstable-v1.xml | 207 + run.in | 41 + screenshots/phosh-locked.png | Bin 0 -> 9830 bytes screenshots/phosh-overview.png | Bin 0 -> 104279 bytes searchd/meson.build | 15 + searchd/search-provider.c | 684 + searchd/search-provider.h | 64 + searchd/searchd.c | 900 ++ searchd/searchd.h | 21 + src/activity.c | 880 ++ src/activity.h | 25 + src/ambient.c | 606 + src/ambient.h | 22 + src/animation.c | 265 + src/animation.h | 59 + src/app-auth-prompt.c | 478 + src/app-auth-prompt.h | 33 + src/app-grid-base-button.c | 249 + src/app-grid-base-button.h | 27 + src/app-grid-button.c | 799 + src/app-grid-button.h | 59 + src/app-grid-folder-button.c | 207 + src/app-grid-folder-button.h | 19 + src/app-grid.c | 823 + src/app-grid.h | 34 + src/app-list-model.c | 366 + src/app-list-model.h | 30 + src/app-tracker.c | 743 + src/app-tracker.h | 23 + src/arrow.c | 189 + src/arrow.h | 23 + src/audio-manager.c | 158 + src/audio-manager.h | 29 + src/audio/audio-device.c | 187 + src/audio/audio-device.h | 25 + src/audio/audio-devices.c | 343 + src/audio/audio-devices.h | 22 + src/auth-prompt-option.c | 125 + src/auth-prompt-option.h | 22 + src/auth.c | 195 + src/auth.h | 27 + src/auto-brightness-bucket.c | 217 + src/auto-brightness-bucket.h | 22 + src/auto-brightness.c | 78 + src/auto-brightness.h | 37 + src/background-cache.c | 214 + src/background-cache.h | 35 + src/background-image.c | 243 + src/background-image.h | 33 + src/background-manager.c | 561 + src/background-manager.h | 29 + src/background.c | 445 + src/background.h | 44 + src/backlight-priv.h | 33 + src/backlight-sysfs.c | 326 + src/backlight-sysfs.h | 20 + src/backlight.c | 415 + src/backlight.h | 34 + src/battery-info.c | 227 + src/battery-info.h | 23 + src/battery-manager.c | 232 + src/battery-manager.h | 21 + src/bidi.c | 80 + src/bidi.h | 17 + src/brightness-manager.c | 897 ++ src/brightness-manager.h | 29 + src/brightness-settings.c | 146 + src/brightness-settings.h | 21 + src/bt-device-row.c | 274 + src/bt-device-row.h | 20 + src/bt-info.c | 260 + src/bt-info.h | 20 + src/bt-manager.c | 590 + src/bt-manager.h | 41 + src/bt-status-page.c | 166 + src/bt-status-page.h | 24 + src/call-notification.c | 295 + src/call-notification.h | 22 + src/call.c | 493 + src/call.h | 20 + src/calls-manager.c | 539 + src/calls-manager.h | 44 + src/cell-broadcast-manager.c | 173 + src/cell-broadcast-manager.h | 20 + src/cell-broadcast-prompt.c | 179 + src/cell-broadcast-prompt.h | 17 + src/clamp.c | 318 + src/clamp.h | 22 + src/connectivity-info.c | 139 + src/connectivity-info.h | 20 + src/connectivity-manager.c | 320 + src/connectivity-manager.h | 22 + .../gnome-bluetooth/bluetooth-client.h | 48 + .../gnome-bluetooth/bluetooth-device.h | 17 + src/contrib/gnome-bluetooth/bluetooth-enums.h | 132 + .../gnome-bluetooth-enum-types.h | 28 + src/contrib/shell-network-agent.c | 899 ++ src/contrib/shell-network-agent.h | 77 + src/dbus/meson.build | 238 + src/dbus/mobi.phosh.Shell.DebugControl.xml | 21 + src/dbus/mobi.phosh.Shell.Search.xml | 104 + src/dbus/net.hadess.SensorProxy.xml | 215 + src/dbus/org.Gtk.MountOperationHandler.xml | 32 + src/dbus/org.freedesktop.Accounts.User.xml | 38 + src/dbus/org.freedesktop.Accounts.xml | 29 + src/dbus/org.freedesktop.GeoClue2.Agent.xml | 11 + src/dbus/org.freedesktop.GeoClue2.Manager.xml | 9 + src/dbus/org.freedesktop.Notifications.xml | 43 + src/dbus/org.freedesktop.hostname1.xml | 9 + src/dbus/org.freedesktop.impl.portal.xml | 23 + src/dbus/org.freedesktop.login1.Manager.xml | 31 + src/dbus/org.freedesktop.login1.Session.xml | 27 + src/dbus/org.gnome.Calls.Call.xml | 80 + src/dbus/org.gnome.Calls.EmergencyCalls.xml | 41 + src/dbus/org.gnome.Mutter.DisplayConfig.xml | 509 + src/dbus/org.gnome.Mutter.IdleMonitor.xml | 35 + src/dbus/org.gnome.ScreenSaver.xml | 23 + ...org.gnome.SessionManager.ClientPrivate.xml | 123 + ....gnome.SessionManager.EndSessionDialog.xml | 57 + .../org.gnome.SessionManager.Presence.xml | 49 + src/dbus/org.gnome.SessionManager.xml | 38 + src/dbus/org.gnome.SettingsDaemon.Color.xml | 6 + src/dbus/org.gnome.SettingsDaemon.Rfkill.xml | 10 + src/dbus/org.gnome.Shell.Brightness.xml | 45 + src/dbus/org.gnome.Shell.Screenshot.xml | 145 + src/dbus/org.gnome.Shell.SearchProvider2.xml | 87 + src/dbus/org.gnome.Shell.xml | 56 + src/dbus/org.mpris.MediaPlayer2.xml | 28 + src/dbus/org.ofono.xml | 34 + src/dbus/sm.puri.OSK0.xml | 31 + src/debug-control.c | 186 + src/debug-control.h | 26 + src/default-media-player.c | 88 + src/default-media-player.h | 20 + src/docked-info.c | 207 + src/docked-info.h | 20 + src/docked-manager.c | 329 + src/docked-manager.h | 25 + src/drag-surface.c | 579 + src/drag-surface.h | 76 + src/emergency-calls-manager.c | 453 + src/emergency-calls-manager.h | 27 + src/emergency-contact-row.c | 185 + src/emergency-contact-row.h | 27 + src/emergency-contact.c | 222 + src/emergency-contact.h | 28 + src/emergency-menu.c | 272 + src/emergency-menu.h | 21 + src/end-session-dialog.c | 586 + src/end-session-dialog.h | 45 + src/fader.c | 268 + src/fader.h | 21 + src/fading-label.c | 327 + src/fading-label.h | 28 + src/fake-clock.c | 194 + src/fake-clock.h | 21 + src/favorite-list-model.c | 314 + src/favorite-list-model.h | 30 + src/feedback-manager.c | 359 + src/feedback-manager.h | 27 + src/feedback-status-page.c | 202 + src/feedback-status-page.h | 20 + src/feedbackinfo.c | 218 + src/feedbackinfo.h | 20 + src/folder-info.c | 478 + src/folder-info.h | 28 + src/gnome-shell-manager.c | 805 + src/gnome-shell-manager.h | 84 + src/gtk-list-models/gtkfilterlistmodel.c | 711 + src/gtk-list-models/gtkfilterlistmodel.h | 74 + src/gtk-list-models/gtkrbtree.c | 798 + src/gtk-list-models/gtkrbtreeprivate.h | 75 + src/gtk-list-models/gtksortlistmodel.c | 562 + src/gtk-list-models/gtksortlistmodel.h | 61 + src/gtk-list-models/meson.build | 10 + src/gtk-mount-manager.c | 414 + src/gtk-mount-manager.h | 18 + src/gtk-mount-prompt.c | 553 + src/gtk-mount-prompt.h | 28 + src/hks-info.c | 210 + src/hks-info.h | 20 + src/hks-manager.c | 513 + src/hks-manager.h | 31 + src/home.c | 789 + src/home.h | 39 + src/idle-manager.c | 455 + src/idle-manager.h | 17 + src/keyboard-events.c | 314 + src/keyboard-events.h | 26 + src/keypad.c | 507 + src/keypad.h | 31 + src/launcher-entry-manager.c | 171 + src/launcher-entry-manager.h | 24 + src/layersurface-priv.h | 50 + src/layersurface.c | 1015 ++ src/layersurface.h | 42 + src/layout-manager.c | 481 + src/layout-manager.h | 28 + src/libphosh-abi.dump | 6157 ++++++++ src/libphosh.h | 32 + src/libphosh.syms.in | 39 + src/location-info.c | 164 + src/location-info.h | 20 + src/location-manager.c | 526 + src/location-manager.h | 20 + src/lockscreen-bg.c | 193 + src/lockscreen-bg.h | 26 + src/lockscreen-manager-priv.h | 16 + src/lockscreen-manager.c | 657 + src/lockscreen-manager.h | 31 + src/lockscreen-priv.h | 19 + src/lockscreen.c | 1428 ++ src/lockscreen.h | 71 + src/lockshield.c | 75 + src/lockshield.h | 17 + src/main.c | 163 + src/manager.c | 85 + src/manager.h | 29 + src/media-player.c | 1044 ++ src/media-player.h | 44 + src/meson.build | 681 + src/metainfo-cache.c | 199 + src/metainfo-cache.h | 21 + src/mode-manager.c | 471 + src/mode-manager.h | 67 + src/monitor-manager.c | 1709 ++ src/monitor-manager.h | 59 + src/monitor/gamma-table.c | 320 + src/monitor/gamma-table.h | 15 + src/monitor/head-priv.h | 83 + src/monitor/head.c | 683 + src/monitor/meson.build | 5 + src/monitor/monitor.c | 831 + src/monitor/monitor.h | 170 + src/mount-manager.c | 324 + src/mount-manager.h | 20 + src/mount-operation.c | 145 + src/mount-operation.h | 19 + src/mpris-manager.c | 506 + src/mpris-manager.h | 33 + src/network-auth-manager.c | 763 + src/network-auth-manager.h | 19 + src/network-auth-prompt.c | 529 + src/network-auth-prompt.h | 44 + src/notifications/dbus-notification.c | 145 + src/notifications/dbus-notification.h | 33 + src/notifications/meson.build | 29 + src/notifications/mount-notification.c | 154 + src/notifications/mount-notification.h | 20 + src/notifications/notification-banner.c | 299 + src/notifications/notification-banner.h | 25 + src/notifications/notification-content.c | 474 + src/notifications/notification-content.h | 30 + src/notifications/notification-frame.c | 618 + src/notifications/notification-frame.h | 36 + src/notifications/notification-list.c | 322 + src/notifications/notification-list.h | 31 + src/notifications/notification-source.c | 265 + src/notifications/notification-source.h | 29 + src/notifications/notification.c | 1134 ++ src/notifications/notification.h | 133 + src/notifications/notify-feedback.c | 510 + src/notifications/notify-feedback.h | 26 + src/notifications/notify-manager.c | 1027 ++ src/notifications/notify-manager.h | 44 + src/notifications/timestamp-label-priv.h | 16 + src/notifications/timestamp-label.c | 394 + src/notifications/timestamp-label.h | 23 + src/osd-window.c | 296 + src/osd-window.h | 20 + src/osk-manager.c | 322 + src/osk-manager.h | 24 + src/overview.c | 903 ++ src/overview.h | 28 + src/password-entry.c | 68 + src/password-entry.h | 19 + src/phosh-exported-symbols.txt.in | 105 + src/phosh-marshalers.list | 2 + src/phosh-settings-enums.h | 70 + src/phosh-wayland.c | 590 + src/phosh-wayland.h | 80 + src/phosh.gresources.xml | 101 + src/plugin-loader.c | 220 + src/plugin-loader.h | 28 + src/plugin-shell.h | 33 + src/polkit-auth-agent.c | 445 + src/polkit-auth-agent.h | 25 + src/polkit-auth-prompt.c | 657 + src/polkit-auth-prompt.h | 24 + src/portal-access-manager.c | 232 + src/portal-access-manager.h | 18 + src/portal-request.c | 135 + src/portal-request.h | 21 + src/power-menu-manager.c | 245 + src/power-menu-manager.h | 14 + src/power-menu.c | 173 + src/power-menu.h | 23 + src/proximity.c | 381 + src/proximity.h | 22 + src/quick-setting.c | 709 + src/quick-setting.h | 50 + src/quick-settings-box.c | 911 ++ src/quick-settings-box.h | 28 + src/quick-settings.c | 337 + src/quick-settings.h | 19 + src/revealer.c | 362 + src/revealer.h | 30 + src/rotateinfo.c | 255 + src/rotateinfo.h | 34 + src/rotation-manager.c | 754 + src/rotation-manager.h | 47 + src/run-command-dialog.c | 132 + src/run-command-dialog.h | 24 + src/run-command-manager.c | 191 + src/run-command-manager.h | 16 + src/screen-saver-manager.c | 1023 ++ src/screen-saver-manager.h | 26 + src/screenshot-manager.c | 1382 ++ src/screenshot-manager.h | 27 + src/search/meson.build | 31 + src/search/search-client.c | 541 + src/search/search-client.h | 60 + src/search/search-result-meta.c | 280 + src/search/search-result-meta.h | 39 + src/search/search-source.c | 205 + src/search/search-source.h | 35 + src/sensor-proxy-manager.c | 64 + src/sensor-proxy-manager.h | 18 + src/session-manager.c | 547 + src/session-manager.h | 43 + src/session-presence.c | 80 + src/session-presence.h | 35 + src/settings.c | 516 + src/settings.h | 18 + src/settings/audio-device-row.c | 184 + src/settings/audio-device-row.h | 22 + src/settings/audio-settings.c | 436 + src/settings/audio-settings.h | 21 + src/settings/channel-bar.c | 386 + src/settings/channel-bar.h | 45 + src/settings/meson.build | 7 + src/shell-priv.h | 141 + src/shell.c | 2833 ++++ src/shell.h | 57 + src/splash-manager.c | 318 + src/splash-manager.h | 24 + src/splash.c | 342 + src/splash.h | 30 + src/status-icon.c | 490 + src/status-icon.h | 51 + src/status-page-placeholder.c | 299 + src/status-page-placeholder.h | 27 + src/status-page.c | 403 + src/status-page.h | 43 + src/style-manager.c | 283 + src/style-manager.h | 23 + src/stylesheet/adwaita-dark.css | 22 + src/stylesheet/adwaita-hc-light.css | 69 + src/stylesheet/common.css | 1286 ++ src/suspend-manager.c | 219 + src/suspend-manager.h | 19 + src/swipe-away-bin.c | 630 + src/swipe-away-bin.h | 30 + src/system-modal-dialog.c | 463 + src/system-modal-dialog.h | 33 + src/system-modal.c | 202 + src/system-modal.h | 26 + src/system-prompt.c | 718 + src/system-prompt.h | 20 + src/system-prompter.c | 131 + src/system-prompter.h | 13 + src/thumbnail-priv.h | 17 + src/thumbnail.c | 131 + src/thumbnail.h | 30 + src/top-panel-bg.c | 119 + src/top-panel-bg.h | 24 + src/top-panel.c | 1119 ++ src/top-panel.h | 42 + src/toplevel-manager.c | 461 + src/toplevel-manager.h | 29 + src/toplevel-thumbnail.c | 265 + src/toplevel-thumbnail.h | 24 + src/toplevel.c | 469 + src/toplevel.h | 31 + src/torch-info.c | 233 + src/torch-info.h | 20 + src/torch-manager.c | 445 + src/torch-manager.h | 30 + src/udev-manager.c | 271 + src/udev-manager.h | 27 + src/ui/activity.ui | 138 + src/ui/app-auth-prompt.ui | 80 + src/ui/app-grid-base-button.ui | 44 + src/ui/app-grid-button.ui | 58 + src/ui/app-grid-folder-button.ui | 23 + src/ui/app-grid.ui | 298 + src/ui/audio-device-row.ui | 41 + src/ui/audio-settings.ui | 148 + src/ui/brightness-settings.ui | 132 + src/ui/bt-device-row.ui | 34 + src/ui/bt-status-page.ui | 58 + src/ui/call-notification.ui | 120 + src/ui/cell-broadcast-prompt.ui | 30 + src/ui/channel-bar.ui | 33 + src/ui/emergency-contact-row.ui | 15 + src/ui/emergency-menu.ui | 159 + src/ui/end-session-dialog.ui | 79 + src/ui/feedback-status-page.ui | 57 + src/ui/gtk-mount-prompt.ui | 143 + src/ui/home.ui | 77 + src/ui/keypad.ui | 213 + src/ui/lockscreen.ui | 384 + src/ui/media-player.ui | 230 + src/ui/network-auth-prompt.ui | 99 + src/ui/notification-content.ui | 84 + src/ui/notification-frame.ui | 101 + src/ui/osd-window.ui | 66 + src/ui/overview.ui | 35 + src/ui/password-entry.ui | 12 + src/ui/polkit-auth-prompt.ui | 113 + src/ui/power-menu.ui | 260 + src/ui/quick-setting.ui | 56 + src/ui/quick-settings-box.ui | 9 + src/ui/quick-settings.ui | 146 + src/ui/revealer.ui | 15 + src/ui/run-command-dialog.ui | 32 + src/ui/settings.ui | 209 + src/ui/splash.ui | 37 + src/ui/status-icon.ui | 18 + src/ui/status-page-placeholder.ui | 38 + src/ui/status-page.ui | 53 + src/ui/system-modal-dialog.ui | 57 + src/ui/system-prompt.ui | 154 + src/ui/timestamp-label.ui | 12 + src/ui/top-panel.ui | 445 + src/ui/widget-box.ui | 19 + src/ui/wifi-network-row.ui | 34 + src/ui/wifi-status-page.ui | 93 + src/util.c | 1146 ++ src/util.h | 101 + src/vpn-info.c | 238 + src/vpn-info.h | 20 + src/vpn-manager.c | 541 + src/vpn-manager.h | 24 + src/wall-clock-priv.h | 15 + src/wall-clock.c | 347 + src/wall-clock.h | 48 + src/widget-box.c | 231 + src/widget-box.h | 21 + src/wifi-info.c | 245 + src/wifi-info.h | 20 + src/wifi-manager.c | 1415 ++ src/wifi-manager.h | 37 + src/wifi-network-row.c | 172 + src/wifi-network-row.h | 22 + src/wifi-network.c | 470 + src/wifi-network.h | 41 + src/wifi-status-page.c | 257 + src/wifi-status-page.h | 19 + src/wl-buffer.c | 119 + src/wl-buffer.h | 39 + src/wwan-info.c | 362 + src/wwan-info.h | 22 + src/wwan/meson.build | 15 + src/wwan/phosh-wwan-iface.c | 240 + src/wwan/phosh-wwan-iface.h | 54 + src/wwan/phosh-wwan-mm.c | 792 + src/wwan/phosh-wwan-mm.h | 20 + src/wwan/phosh-wwan-ofono.c | 733 + src/wwan/phosh-wwan-ofono.h | 20 + src/wwan/wwan-manager.c | 540 + src/wwan/wwan-manager.h | 27 + subprojects/glib.wrap | 5 + subprojects/gmobile.wrap | 5 + subprojects/gvc.wrap | 5 + subprojects/libcall-ui.wrap | 5 + subprojects/libfeedback.wrap | 4 + subprojects/libhandy.wrap | 5 + tests/data/cat.jpg | Bin 0 -> 84614 bytes tests/data/keymap.txt | 1461 ++ tests/data/phoc-landscape.ini | 11 + tests/data/phoc-notch.ini | 3 + tests/data/phosh,tiny.json | 14 + tests/integration/__init__.py | 180 + tests/integration/keyfile | 2 + tests/integration/meson.build | 8 + tests/integration/run-pytest.in | 11 + tests/integration/test_dbus.py | 409 + tests/integration/test_output.py | 74 + tests/meson.build | 402 + tests/mock-mm-nm.py | 53 + tests/phosh-logo.png | Bin 0 -> 31347 bytes tests/phosh-test.gresources.xml | 7 + tests/screenshot-no-anim-overrides.css | 6 + tests/services/meson.build | 27 + ....gnome.Phosh.MockSearchProvider.service.in | 3 + tests/services/testlib-search-provider-app.c | 114 + tests/services/testlib-search-provider-app.h | 21 + tests/services/testlib-search-provider.c | 218 + tests/services/testlib-search-provider.h | 29 + tests/stubs/app-tracker.c | 32 + tests/stubs/background-manager.c | 48 + tests/stubs/bad-instance.h | 46 + tests/stubs/bad-prop.h | 61 + tests/stubs/evil-icon.c | 73 + tests/stubs/evil-icon.h | 23 + tests/stubs/lockscreen-manager.c | 16 + tests/stubs/meson.build | 20 + tests/stubs/phosh.c | 169 + tests/stubs/thumbnail.c | 56 + tests/stubs/toplevel-manager.c | 87 + tests/stubs/toplevel.c | 115 + tests/system/config/meson.build | 7 + tests/system/meson.build | 1 + .../share/applications/demo.app.First.desktop | 28 + .../desktop-directories/X-Phosh-foo.directory | 4 + .../desktop-directories/broken.directory | 4 + tests/test-activity.c | 29 + tests/test-app-auth-prompt.c | 40 + tests/test-app-grid-button.c | 363 + tests/test-app-grid-folder-button.c | 58 + tests/test-app-list-model.c | 100 + tests/test-auto-brightness-bucket.c | 56 + tests/test-background.c | 80 + tests/test-calls-manager.c | 212 + tests/test-connectivity-info.c | 31 + tests/test-css.c | 54 + tests/test-drag-surface.c | 106 + tests/test-end-session-dialog.c | 94 + tests/test-fading-label.c | 35 + tests/test-favourite-model.c | 284 + tests/test-folder-info.c | 261 + tests/test-gamma-table.c | 37 + tests/test-gtk-mount-manager.c | 358 + tests/test-head.c | 143 + tests/test-idle-manager.c | 154 + tests/test-keypad.c | 124 + tests/test-layer-surface.c | 179 + tests/test-lockscreen.c | 150 + tests/test-lockshield.c | 34 + tests/test-media-player.c | 33 + tests/test-monitor-manager.c | 81 + tests/test-mount-notification.c | 96 + tests/test-notification-banner.c | 156 + tests/test-notification-content.c | 256 + tests/test-notification-frame.c | 175 + tests/test-notification-list.c | 309 + tests/test-notification-source.c | 144 + tests/test-notification.c | 467 + tests/test-notify-feedback.c | 118 + tests/test-notify-manager.c | 172 + tests/test-osd-window.c | 46 + tests/test-overview.c | 31 + tests/test-plugin-loader.c | 75 + tests/test-power-menu.c | 30 + tests/test-quick-setting.c | 350 + tests/test-quick-settings-box.c | 229 + tests/test-screenshot-manager.c | 93 + tests/test-search-provider.c | 263 + tests/test-search-result-meta.c | 338 + tests/test-search-source.c | 157 + tests/test-shell.c | 202 + tests/test-status-icon.c | 145 + tests/test-system-modal-dialog.c | 57 + tests/test-system-modal.c | 60 + tests/test-take-screenshots.c | 800 + tests/test-timestamp-label.c | 179 + tests/test-util.c | 146 + tests/test-wall-clock.c | 69 + tests/testlib-calls-mock.c | 76 + tests/testlib-calls-mock.h | 25 + tests/testlib-compositor.c | 51 + tests/testlib-compositor.h | 25 + tests/testlib-emergency-calls.c | 144 + tests/testlib-emergency-calls.h | 27 + tests/testlib-full-shell.c | 192 + tests/testlib-full-shell.h | 41 + tests/testlib-head-stub.c | 218 + tests/testlib-head-stub.h | 18 + tests/testlib-mpris-mock.c | 93 + tests/testlib-mpris-mock.h | 27 + tests/testlib-wait-for-shell-state.c | 92 + tests/testlib-wait-for-shell-state.h | 29 + tests/testlib.c | 492 + tests/testlib.h | 42 + .../glib-2.0/settings/keyfile | 3 + .../glib-2.0/settings/meson.build | 6 + tests/user/config/meson.build | 7 + tests/user/meson.build | 2 + .../applications/demo.app.Second.desktop | 13 + tools/app-buttons.c | 119 + tools/app-grid-standalone.c | 63 + tools/app-scroll.c | 86 + tools/build-symbols-file | 21 + tools/check-access-portal | 59 + tools/check-deprecated-ui-props | 28 + tools/check-end-session | 11 + tools/check-exported-symbols | 15 + tools/check-license-headers.py | 94 + tools/check-mount-operation | 57 + tools/check-notification | 57 + tools/check-osd | 22 + tools/check-screen-saver | 16 + tools/check-screenshot | 14 + tools/custom-quick-settings-standalone.c | 144 + tools/dump-app-list.c | 47 + tools/image-notify.c | 73 + tools/meson.build | 117 + tools/montage-screenshots | 9 + tools/notify-blocks.c | 132 + tools/notify-server-standalone.c | 104 + tools/plugin-prefs-standalone.c | 198 + tools/quick-settings-box-standalone.c | 298 + tools/run_tool.in | 12 + tools/search.c | 297 + tools/set_brightness | 11 + tools/widget-box-standalone.c | 95 + 1020 files changed, 203441 insertions(+) create mode 100644 .dir-locals.el create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .github/FUNDING.yml create mode 100644 .gitignore create mode 100644 .gitlab-ci.yml create mode 100644 .gitlab-ci/Makefile create mode 100644 .gitlab-ci/README.md create mode 100644 .gitlab-ci/asan.Dockerfile create mode 100755 .gitlab-ci/check-consistency create mode 100755 .gitlab-ci/check-meson create mode 100755 .gitlab-ci/check-po create mode 100755 .gitlab-ci/check-style.py create mode 100755 .gitlab-ci/check-ui create mode 100644 .gitlab-ci/commit-rules.yml create mode 100644 .gitlab-ci/debian.Dockerfile create mode 100755 .gitlab-ci/g-style create mode 100644 .gitlab-ci/libphosh-test.c create mode 100755 .gitlab-ci/run-docker.sh create mode 100644 .gitlab-ci/screenshot.Dockerfile create mode 100644 .gitlab-ci/uncrustify.cfg create mode 100644 .gitlab/issue_templates/bug.md create mode 100644 .gitmodules create mode 100644 .ruff.toml create mode 100644 COPYING create mode 100644 HACKING.md create mode 100644 NEWS create mode 100644 README.md create mode 100644 calendar-server/CalendarServer.service.in create mode 100644 calendar-server/calendar-debug.h create mode 100644 calendar-server/calendar-server.c create mode 100644 calendar-server/calendar-sources.c create mode 100644 calendar-server/calendar-sources.h create mode 100644 calendar-server/meson.build create mode 100644 data/00_mobi.Phosh.gschema.override create mode 100644 data/icons/app-close-symbolic.svg create mode 100644 data/icons/app-icon-unknown-symbolic.svg create mode 100644 data/icons/app-icon-unknown.svg create mode 100644 data/icons/asterisk-symbolic.svg create mode 100644 data/icons/audio-handsfree-symbolic.svg create mode 100644 data/icons/auth-sim-locked-symbolic.svg create mode 100644 data/icons/auth-sim-missing-symbolic.svg create mode 100644 data/icons/auto-brightness-symbolic.svg create mode 100644 data/icons/camera-hardware-disabled-symbolic.svg create mode 100644 data/icons/chat-none-symbolic.svg create mode 100644 data/icons/chat-symbolic.svg create mode 100644 data/icons/eye-not-looking-symbolic.svg create mode 100644 data/icons/eye-open-negative-filled-symbolic.svg create mode 100644 data/icons/feedback-quiet-symbolic.svg create mode 100644 data/icons/input-powerbar-symbolic.svg create mode 100644 data/icons/meson.build create mode 100644 data/icons/microphone-hardware-disabled-symbolic.svg create mode 100644 data/icons/mobi.phosh.Shell-symbolic.svg create mode 100644 data/icons/mobile-data-disabled-symbolic.svg create mode 100644 data/icons/mobile-data-symbolic.svg create mode 100644 data/icons/moon-filled-symbolic.svg create mode 100644 data/icons/network-cellular-disabled-symbolic.svg create mode 100644 data/icons/network-cellular-no-data-signal-excellent-symbolic.svg create mode 100644 data/icons/network-cellular-no-data-signal-good-symbolic.svg create mode 100644 data/icons/network-cellular-no-data-signal-none-symbolic.svg create mode 100644 data/icons/network-cellular-no-data-signal-ok-symbolic.svg create mode 100644 data/icons/network-cellular-no-data-signal-weak-symbolic.svg create mode 100644 data/icons/network-vpn-disabled-symbolic.svg create mode 100644 data/icons/network-wireless-disabled-symbolic.svg create mode 100644 data/icons/no-notifications-symbolic.svg create mode 100644 data/icons/padlock-symbolic.svg create mode 100644 data/icons/phone-docked-symbolic.svg create mode 100644 data/icons/phone-undocked-symbolic.svg create mode 100644 data/icons/screen-rotation-landscape-symbolic.svg create mode 100644 data/icons/screen-rotation-portrait-symbolic.svg create mode 100644 data/icons/screenshot-portrait-symbolic.svg create mode 100644 data/icons/settings-symbolic.svg create mode 100644 data/icons/skip-backwards-10-symbolic.svg create mode 100644 data/icons/skip-forward-30-symbolic.svg create mode 100644 data/icons/splash-process-working-symbolic.svg create mode 100644 data/icons/swipe-arrow-symbolic.svg create mode 100644 data/icons/torch-disabled-symbolic.svg create mode 100644 data/icons/torch-enabled-symbolic.svg create mode 100644 data/leak-suppress.txt create mode 100644 data/meson.build create mode 100644 data/mobi.phosh.Shell.Search.service.in create mode 100644 data/mobi.phosh.Shell.desktop.in.in create mode 100644 data/mobi.phosh.shell.gschema.xml create mode 100644 data/phoc.ini create mode 100644 data/phosh-portals.conf create mode 100755 data/phosh-session.in create mode 100644 data/phosh-shell.portal create mode 100644 data/phosh.service create mode 100644 data/phosh.session.desktop.in.in create mode 100644 data/systemd/meson.build create mode 100644 data/systemd/mobi.phosh.OSK.target create mode 100644 data/systemd/mobi.phosh.Shell.service.in create mode 100644 data/systemd/mobi.phosh.Shell.target create mode 100644 data/systemd/phosh.session.conf.in create mode 100644 data/valgrind.suppressions create mode 100644 data/wayland-sessions/phosh.desktop create mode 100644 debian/README.source create mode 100644 debian/changelog create mode 100644 debian/clean create mode 100644 debian/control create mode 100644 debian/copyright create mode 100644 debian/gbp.conf create mode 100644 debian/gir1.2-phosh-0-dev.install create mode 100644 debian/libphosh-0.45-0.install create mode 100644 debian/libphosh-0.45-0.symbols create mode 100644 debian/libphosh-0.45-dev.install create mode 100644 debian/not-installed create mode 100644 debian/phosh-common.install create mode 100644 debian/phosh-dev.install create mode 100644 debian/phosh-doc.install create mode 100644 debian/phosh-doc.links create mode 100644 debian/phosh-mobile-tweaks.install create mode 100644 debian/phosh-plugins.install create mode 100644 debian/phosh.dirs create mode 100644 debian/phosh.docs create mode 100644 debian/phosh.install create mode 100644 debian/phosh.manpages create mode 100644 debian/phosh.postinst.in create mode 100644 debian/phosh.postrm.in create mode 100644 debian/phosh.triggers.in create mode 100755 debian/rules create mode 100644 debian/source/format create mode 100644 debian/tests/control create mode 100644 docs/app-dev.md create mode 100644 docs/gettingstarted.md create mode 100644 docs/meson.build create mode 100644 docs/phosh-dbus-sm.puri.OSK0.md create mode 100644 docs/phosh-session.rst create mode 100644 docs/phosh.gsettings.rst create mode 100644 docs/phosh.rst create mode 100644 docs/phosh.toml.in create mode 100644 docs/urlmap.js create mode 100644 gcovr.cfg create mode 100644 meson.build create mode 100644 meson_options.txt create mode 100644 phosh.doap create mode 100644 plugins/caffeine-quick-setting/caffeine-quick-setting.c create mode 100644 plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in create mode 100644 plugins/caffeine-quick-setting/caffeine-quick-setting.h create mode 100644 plugins/caffeine-quick-setting/icons/cafe-cold-symbolic.svg create mode 100644 plugins/caffeine-quick-setting/icons/cafe-hot-symbolic.svg create mode 100644 plugins/caffeine-quick-setting/interval-row.c create mode 100644 plugins/caffeine-quick-setting/interval-row.h create mode 100644 plugins/caffeine-quick-setting/interval-row.ui create mode 100644 plugins/caffeine-quick-setting/meson.build create mode 100644 plugins/caffeine-quick-setting/mobi.phosh.plugins.caffeine-quick-setting.gschema.xml create mode 100644 plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.c create mode 100644 plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.gresources.xml create mode 100644 plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c create mode 100644 plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.h create mode 100644 plugins/caffeine-quick-setting/prefs/meson.build create mode 100644 plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.c create mode 100644 plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.gresources.xml create mode 100644 plugins/caffeine-quick-setting/prefs/prefs.ui create mode 100644 plugins/caffeine-quick-setting/prefs/stylesheet/common.css create mode 100644 plugins/caffeine-quick-setting/qs.ui create mode 100644 plugins/calendar/calendar.c create mode 100644 plugins/calendar/calendar.desktop.in.in create mode 100644 plugins/calendar/calendar.h create mode 100644 plugins/calendar/calendar.ui create mode 100644 plugins/calendar/meson.build create mode 100644 plugins/calendar/phosh-plugin-calendar.c create mode 100644 plugins/calendar/phosh-plugin-calendar.gresources.xml create mode 100644 plugins/calendar/stylesheet/common.css create mode 100644 plugins/dark-mode-quick-setting/dark-mode-quick-setting.c create mode 100644 plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in create mode 100644 plugins/dark-mode-quick-setting/dark-mode-quick-setting.h create mode 100644 plugins/dark-mode-quick-setting/icons/dark-mode-disabled-symbolic.svg create mode 100644 plugins/dark-mode-quick-setting/icons/dark-mode-symbolic.svg create mode 100644 plugins/dark-mode-quick-setting/meson.build create mode 100644 plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.c create mode 100644 plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.gresources.xml create mode 100644 plugins/dark-mode-quick-setting/qs.ui create mode 100644 plugins/emergency-info/emergency-info-common.h create mode 100644 plugins/emergency-info/emergency-info-row.c create mode 100644 plugins/emergency-info/emergency-info-row.h create mode 100644 plugins/emergency-info/emergency-info-row.ui create mode 100644 plugins/emergency-info/emergency-info.c create mode 100644 plugins/emergency-info/emergency-info.desktop.in.in create mode 100644 plugins/emergency-info/emergency-info.h create mode 100644 plugins/emergency-info/emergency-info.ui create mode 100644 plugins/emergency-info/meson.build create mode 100644 plugins/emergency-info/phosh-plugin-emergency-info.c create mode 100644 plugins/emergency-info/phosh-plugin-emergency-info.gresources.xml create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs-row.c create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs-row.h create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs-row.ui create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs.c create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs.h create mode 100644 plugins/emergency-info/prefs/emergency-info-prefs.ui create mode 100644 plugins/emergency-info/prefs/meson.build create mode 100644 plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.c create mode 100644 plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.gresources.xml create mode 100644 plugins/emergency-info/stylesheet/common.css create mode 100644 plugins/launcher-box/launcher-box.c create mode 100644 plugins/launcher-box/launcher-box.desktop.in.in create mode 100644 plugins/launcher-box/launcher-box.h create mode 100644 plugins/launcher-box/launcher-box.ui create mode 100644 plugins/launcher-box/launcher-item.c create mode 100644 plugins/launcher-box/launcher-item.h create mode 100644 plugins/launcher-box/launcher-row.c create mode 100644 plugins/launcher-box/launcher-row.h create mode 100644 plugins/launcher-box/launcher-row.ui create mode 100644 plugins/launcher-box/meson.build create mode 100644 plugins/launcher-box/phosh-plugin-launcher-box.c create mode 100644 plugins/launcher-box/phosh-plugin-launcher-box.gresources.xml create mode 100644 plugins/launcher-box/sm.puri.phosh.plugins.launcher-box.gschema.xml create mode 100644 plugins/launcher-box/stylesheet/common.css create mode 100644 plugins/location-quick-setting/icons/location-quick-setting-symbolic.svg create mode 100644 plugins/location-quick-setting/location-quick-setting.c create mode 100644 plugins/location-quick-setting/location-quick-setting.desktop.in.in create mode 100644 plugins/location-quick-setting/location-quick-setting.h create mode 100644 plugins/location-quick-setting/meson.build create mode 100644 plugins/location-quick-setting/phosh-plugin-location-quick-setting.c create mode 100644 plugins/location-quick-setting/phosh-plugin-location-quick-setting.gresources.xml create mode 100644 plugins/location-quick-setting/qs.ui create mode 100644 plugins/media-players/media-players.c create mode 100644 plugins/media-players/media-players.desktop.in.in create mode 100644 plugins/media-players/media-players.h create mode 100644 plugins/media-players/media-players.ui create mode 100644 plugins/media-players/meson.build create mode 100644 plugins/media-players/phosh-plugin-media-players.c create mode 100644 plugins/media-players/phosh-plugin-media-players.gresources.xml create mode 100644 plugins/media-players/stylesheet/common.css create mode 100644 plugins/meson.build create mode 120000 plugins/mobile-data-quick-setting/icons/mobile-data-quick-setting-symbolic.svg create mode 100644 plugins/mobile-data-quick-setting/meson.build create mode 100644 plugins/mobile-data-quick-setting/mobile-data-quick-setting.c create mode 100644 plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in create mode 100644 plugins/mobile-data-quick-setting/mobile-data-quick-setting.h create mode 100644 plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.c create mode 100644 plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.gresources.xml create mode 100644 plugins/mobile-data-quick-setting/qs.ui create mode 100644 plugins/night-light-quick-setting/icons/night-light-quick-setting-symbolic.svg create mode 100644 plugins/night-light-quick-setting/meson.build create mode 100644 plugins/night-light-quick-setting/night-light-quick-setting.c create mode 100644 plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in create mode 100644 plugins/night-light-quick-setting/night-light-quick-setting.h create mode 100644 plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.c create mode 100644 plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.gresources.xml create mode 100644 plugins/night-light-quick-setting/qs.ui create mode 100644 plugins/phosh-plugin.h.in create mode 100644 plugins/pomodoro-quick-setting/icons/pomodoro-active-symbolic.svg create mode 100644 plugins/pomodoro-quick-setting/icons/pomodoro-break-symbolic.svg create mode 100644 plugins/pomodoro-quick-setting/icons/pomodoro-off-symbolic.svg create mode 100644 plugins/pomodoro-quick-setting/meson.build create mode 100644 plugins/pomodoro-quick-setting/mobi.phosh.plugins.pomodoro.gschema.xml create mode 100644 plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.c create mode 100644 plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.gresources.xml create mode 100644 plugins/pomodoro-quick-setting/pomodoro-quick-setting.c create mode 100644 plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in create mode 100644 plugins/pomodoro-quick-setting/pomodoro-quick-setting.h create mode 100644 plugins/pomodoro-quick-setting/prefs/meson.build create mode 100644 plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.c create mode 100644 plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.gresources.xml create mode 100644 plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.c create mode 100644 plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.h create mode 100644 plugins/pomodoro-quick-setting/prefs/prefs.ui create mode 100644 plugins/pomodoro-quick-setting/qs.ui create mode 100644 plugins/scaling-quick-setting/icons/screen-scaling-large-symbolic.svg create mode 100644 plugins/scaling-quick-setting/icons/screen-scaling-small-symbolic.svg create mode 100644 plugins/scaling-quick-setting/meson.build create mode 100644 plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.c create mode 100644 plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.gresources.xml create mode 100644 plugins/scaling-quick-setting/qs.ui create mode 100644 plugins/scaling-quick-setting/scale-row.c create mode 100644 plugins/scaling-quick-setting/scale-row.h create mode 100644 plugins/scaling-quick-setting/scale-row.ui create mode 100644 plugins/scaling-quick-setting/scaling-quick-setting.c create mode 100644 plugins/scaling-quick-setting/scaling-quick-setting.desktop.in.in create mode 100644 plugins/scaling-quick-setting/scaling-quick-setting.h create mode 100644 plugins/simple-custom-quick-setting/meson.build create mode 100644 plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.c create mode 100644 plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.gresources.xml create mode 100644 plugins/simple-custom-quick-setting/qs.ui create mode 100644 plugins/simple-custom-quick-setting/simple-custom-quick-setting.c create mode 100644 plugins/simple-custom-quick-setting/simple-custom-quick-setting.desktop.in.in create mode 100644 plugins/simple-custom-quick-setting/simple-custom-quick-setting.h create mode 100644 plugins/ticket-box/meson.build create mode 100644 plugins/ticket-box/phosh-plugin-ticket-box.c create mode 100644 plugins/ticket-box/phosh-plugin-ticket-box.gresources.xml create mode 100644 plugins/ticket-box/prefs/meson.build create mode 100644 plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.c create mode 100644 plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.gresources.xml create mode 100644 plugins/ticket-box/prefs/ticket-box-prefs.c create mode 100644 plugins/ticket-box/prefs/ticket-box-prefs.h create mode 100644 plugins/ticket-box/prefs/ticket-box-prefs.ui create mode 100644 plugins/ticket-box/sm.puri.phosh.plugins.ticket-box.gschema.xml create mode 100644 plugins/ticket-box/stylesheet/common.css create mode 100644 plugins/ticket-box/ticket-box.c create mode 100644 plugins/ticket-box/ticket-box.desktop.in.in create mode 100644 plugins/ticket-box/ticket-box.h create mode 100644 plugins/ticket-box/ticket-box.ui create mode 100644 plugins/ticket-box/ticket-row.c create mode 100644 plugins/ticket-box/ticket-row.h create mode 100644 plugins/ticket-box/ticket.c create mode 100644 plugins/ticket-box/ticket.h create mode 100644 plugins/upcoming-events/calendar-event.c create mode 100644 plugins/upcoming-events/calendar-event.h create mode 100644 plugins/upcoming-events/event-list.c create mode 100644 plugins/upcoming-events/event-list.h create mode 100644 plugins/upcoming-events/event-list.ui create mode 100644 plugins/upcoming-events/icons/upcoming-events-all-symbolic.svg create mode 100644 plugins/upcoming-events/icons/upcoming-events-skip-empty-symbolic.svg create mode 100644 plugins/upcoming-events/meson.build create mode 100644 plugins/upcoming-events/mobi.phosh.Shell.CalendarServer.xml create mode 100644 plugins/upcoming-events/phosh-plugin-upcoming-events.c create mode 100644 plugins/upcoming-events/phosh-plugin-upcoming-events.gresources.xml create mode 100644 plugins/upcoming-events/prefs/meson.build create mode 100644 plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.c create mode 100644 plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.gresources.xml create mode 100644 plugins/upcoming-events/prefs/upcoming-events-prefs.c create mode 100644 plugins/upcoming-events/prefs/upcoming-events-prefs.h create mode 100644 plugins/upcoming-events/prefs/upcoming-events-prefs.ui create mode 100644 plugins/upcoming-events/sm.puri.phosh.plugins.upcoming-events.gschema.xml create mode 100644 plugins/upcoming-events/stylesheet/common.css create mode 100644 plugins/upcoming-events/upcoming-event.c create mode 100644 plugins/upcoming-events/upcoming-event.h create mode 100644 plugins/upcoming-events/upcoming-event.ui create mode 100644 plugins/upcoming-events/upcoming-events.c create mode 100644 plugins/upcoming-events/upcoming-events.desktop.in.in create mode 100644 plugins/upcoming-events/upcoming-events.h create mode 100644 plugins/upcoming-events/upcoming-events.ui create mode 100644 plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-acquiring-symbolic.svg create mode 100644 plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-disabled-symbolic.svg create mode 100644 plugins/wifi-hotspot-quick-setting/icons/wifi-hotspot-quick-setting-symbolic.svg create mode 100644 plugins/wifi-hotspot-quick-setting/meson.build create mode 100644 plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.c create mode 100644 plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.gresources.xml create mode 100644 plugins/wifi-hotspot-quick-setting/qs.ui create mode 100644 plugins/wifi-hotspot-quick-setting/status-page.ui create mode 100644 plugins/wifi-hotspot-quick-setting/style.css create mode 100644 plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c create mode 100644 plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.desktop.in.in create mode 100644 plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.h create mode 100644 plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.c create mode 100644 plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.h create mode 100644 po/LINGUAS create mode 100644 po/POTFILES.in create mode 100644 po/POTFILES.skip create mode 100644 po/am.po create mode 100644 po/ar.po create mode 100644 po/be.po create mode 100644 po/bg.po create mode 100644 po/ca.po create mode 100644 po/cs.po create mode 100644 po/da.po create mode 100644 po/de.po create mode 100644 po/el.po create mode 100644 po/en_GB.po create mode 100644 po/eo.po create mode 100644 po/es.po create mode 100644 po/eu.po create mode 100644 po/fa.po create mode 100644 po/fi.po create mode 100644 po/fr.po create mode 100644 po/fur.po create mode 100644 po/fy.po create mode 100644 po/gl.po create mode 100644 po/he.po create mode 100644 po/hi.po create mode 100644 po/hr.po create mode 100644 po/ht.po create mode 100644 po/hu.po create mode 100644 po/id.po create mode 100644 po/it.po create mode 100644 po/ja.po create mode 100644 po/ka.po create mode 100644 po/ko.po create mode 100644 po/kw.po create mode 100644 po/la.po create mode 100644 po/lv.po create mode 100644 po/meson.build create mode 100644 po/nb.po create mode 100644 po/nl.po create mode 100644 po/oc.po create mode 100644 po/pl.po create mode 100644 po/pt.po create mode 100644 po/pt_BR.po create mode 100644 po/ro.po create mode 100644 po/ru.po create mode 100644 po/sat.po create mode 100644 po/sk.po create mode 100644 po/sl.po create mode 100644 po/sr.po create mode 100644 po/sv.po create mode 100644 po/tr.po create mode 100644 po/uk.po create mode 100644 po/uz.po create mode 100644 po/zh_CN.po create mode 100644 po/zh_Hans_CN.po create mode 100644 po/zh_TW.po create mode 100644 protocol/input-method-unstable-v2.xml create mode 100644 protocol/meson.build create mode 100644 protocol/phoc-device-state-unstable-v1.xml create mode 100644 protocol/phoc-layer-shell-effects-unstable-v1.xml create mode 100644 protocol/phosh-private.xml create mode 100644 protocol/virtual-keyboard-unstable-v1.xml create mode 100644 protocol/wlr-foreign-toplevel-management-unstable-v1.xml create mode 100644 protocol/wlr-gamma-control-unstable-v1.xml create mode 100644 protocol/wlr-layer-shell-unstable-v1.xml create mode 100644 protocol/wlr-output-management-unstable-v1.xml create mode 100644 protocol/wlr-output-power-management-unstable-v1.xml create mode 100644 protocol/wlr-screencopy-unstable-v1.xml create mode 100755 run.in create mode 100644 screenshots/phosh-locked.png create mode 100644 screenshots/phosh-overview.png create mode 100644 searchd/meson.build create mode 100644 searchd/search-provider.c create mode 100644 searchd/search-provider.h create mode 100644 searchd/searchd.c create mode 100644 searchd/searchd.h create mode 100644 src/activity.c create mode 100644 src/activity.h create mode 100644 src/ambient.c create mode 100644 src/ambient.h create mode 100644 src/animation.c create mode 100644 src/animation.h create mode 100644 src/app-auth-prompt.c create mode 100644 src/app-auth-prompt.h create mode 100644 src/app-grid-base-button.c create mode 100644 src/app-grid-base-button.h create mode 100644 src/app-grid-button.c create mode 100644 src/app-grid-button.h create mode 100644 src/app-grid-folder-button.c create mode 100644 src/app-grid-folder-button.h create mode 100644 src/app-grid.c create mode 100644 src/app-grid.h create mode 100644 src/app-list-model.c create mode 100644 src/app-list-model.h create mode 100644 src/app-tracker.c create mode 100644 src/app-tracker.h create mode 100644 src/arrow.c create mode 100644 src/arrow.h create mode 100644 src/audio-manager.c create mode 100644 src/audio-manager.h create mode 100644 src/audio/audio-device.c create mode 100644 src/audio/audio-device.h create mode 100644 src/audio/audio-devices.c create mode 100644 src/audio/audio-devices.h create mode 100644 src/auth-prompt-option.c create mode 100644 src/auth-prompt-option.h create mode 100644 src/auth.c create mode 100644 src/auth.h create mode 100644 src/auto-brightness-bucket.c create mode 100644 src/auto-brightness-bucket.h create mode 100644 src/auto-brightness.c create mode 100644 src/auto-brightness.h create mode 100644 src/background-cache.c create mode 100644 src/background-cache.h create mode 100644 src/background-image.c create mode 100644 src/background-image.h create mode 100644 src/background-manager.c create mode 100644 src/background-manager.h create mode 100644 src/background.c create mode 100644 src/background.h create mode 100644 src/backlight-priv.h create mode 100644 src/backlight-sysfs.c create mode 100644 src/backlight-sysfs.h create mode 100644 src/backlight.c create mode 100644 src/backlight.h create mode 100644 src/battery-info.c create mode 100644 src/battery-info.h create mode 100644 src/battery-manager.c create mode 100644 src/battery-manager.h create mode 100644 src/bidi.c create mode 100644 src/bidi.h create mode 100644 src/brightness-manager.c create mode 100644 src/brightness-manager.h create mode 100644 src/brightness-settings.c create mode 100644 src/brightness-settings.h create mode 100644 src/bt-device-row.c create mode 100644 src/bt-device-row.h create mode 100644 src/bt-info.c create mode 100644 src/bt-info.h create mode 100644 src/bt-manager.c create mode 100644 src/bt-manager.h create mode 100644 src/bt-status-page.c create mode 100644 src/bt-status-page.h create mode 100644 src/call-notification.c create mode 100644 src/call-notification.h create mode 100644 src/call.c create mode 100644 src/call.h create mode 100644 src/calls-manager.c create mode 100644 src/calls-manager.h create mode 100644 src/cell-broadcast-manager.c create mode 100644 src/cell-broadcast-manager.h create mode 100644 src/cell-broadcast-prompt.c create mode 100644 src/cell-broadcast-prompt.h create mode 100644 src/clamp.c create mode 100644 src/clamp.h create mode 100644 src/connectivity-info.c create mode 100644 src/connectivity-info.h create mode 100644 src/connectivity-manager.c create mode 100644 src/connectivity-manager.h create mode 100644 src/contrib/gnome-bluetooth/bluetooth-client.h create mode 100644 src/contrib/gnome-bluetooth/bluetooth-device.h create mode 100644 src/contrib/gnome-bluetooth/bluetooth-enums.h create mode 100644 src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h create mode 100644 src/contrib/shell-network-agent.c create mode 100644 src/contrib/shell-network-agent.h create mode 100644 src/dbus/meson.build create mode 100644 src/dbus/mobi.phosh.Shell.DebugControl.xml create mode 100644 src/dbus/mobi.phosh.Shell.Search.xml create mode 100644 src/dbus/net.hadess.SensorProxy.xml create mode 100644 src/dbus/org.Gtk.MountOperationHandler.xml create mode 100644 src/dbus/org.freedesktop.Accounts.User.xml create mode 100644 src/dbus/org.freedesktop.Accounts.xml create mode 100644 src/dbus/org.freedesktop.GeoClue2.Agent.xml create mode 100644 src/dbus/org.freedesktop.GeoClue2.Manager.xml create mode 100644 src/dbus/org.freedesktop.Notifications.xml create mode 100644 src/dbus/org.freedesktop.hostname1.xml create mode 100644 src/dbus/org.freedesktop.impl.portal.xml create mode 100644 src/dbus/org.freedesktop.login1.Manager.xml create mode 100644 src/dbus/org.freedesktop.login1.Session.xml create mode 100644 src/dbus/org.gnome.Calls.Call.xml create mode 100644 src/dbus/org.gnome.Calls.EmergencyCalls.xml create mode 100644 src/dbus/org.gnome.Mutter.DisplayConfig.xml create mode 100644 src/dbus/org.gnome.Mutter.IdleMonitor.xml create mode 100644 src/dbus/org.gnome.ScreenSaver.xml create mode 100644 src/dbus/org.gnome.SessionManager.ClientPrivate.xml create mode 100644 src/dbus/org.gnome.SessionManager.EndSessionDialog.xml create mode 100644 src/dbus/org.gnome.SessionManager.Presence.xml create mode 100644 src/dbus/org.gnome.SessionManager.xml create mode 100644 src/dbus/org.gnome.SettingsDaemon.Color.xml create mode 100644 src/dbus/org.gnome.SettingsDaemon.Rfkill.xml create mode 100644 src/dbus/org.gnome.Shell.Brightness.xml create mode 100644 src/dbus/org.gnome.Shell.Screenshot.xml create mode 100644 src/dbus/org.gnome.Shell.SearchProvider2.xml create mode 100644 src/dbus/org.gnome.Shell.xml create mode 100644 src/dbus/org.mpris.MediaPlayer2.xml create mode 100644 src/dbus/org.ofono.xml create mode 100644 src/dbus/sm.puri.OSK0.xml create mode 100644 src/debug-control.c create mode 100644 src/debug-control.h create mode 100644 src/default-media-player.c create mode 100644 src/default-media-player.h create mode 100644 src/docked-info.c create mode 100644 src/docked-info.h create mode 100644 src/docked-manager.c create mode 100644 src/docked-manager.h create mode 100644 src/drag-surface.c create mode 100644 src/drag-surface.h create mode 100644 src/emergency-calls-manager.c create mode 100644 src/emergency-calls-manager.h create mode 100644 src/emergency-contact-row.c create mode 100644 src/emergency-contact-row.h create mode 100644 src/emergency-contact.c create mode 100644 src/emergency-contact.h create mode 100644 src/emergency-menu.c create mode 100644 src/emergency-menu.h create mode 100644 src/end-session-dialog.c create mode 100644 src/end-session-dialog.h create mode 100644 src/fader.c create mode 100644 src/fader.h create mode 100644 src/fading-label.c create mode 100644 src/fading-label.h create mode 100644 src/fake-clock.c create mode 100644 src/fake-clock.h create mode 100644 src/favorite-list-model.c create mode 100644 src/favorite-list-model.h create mode 100644 src/feedback-manager.c create mode 100644 src/feedback-manager.h create mode 100644 src/feedback-status-page.c create mode 100644 src/feedback-status-page.h create mode 100644 src/feedbackinfo.c create mode 100644 src/feedbackinfo.h create mode 100644 src/folder-info.c create mode 100644 src/folder-info.h create mode 100644 src/gnome-shell-manager.c create mode 100644 src/gnome-shell-manager.h create mode 100644 src/gtk-list-models/gtkfilterlistmodel.c create mode 100644 src/gtk-list-models/gtkfilterlistmodel.h create mode 100644 src/gtk-list-models/gtkrbtree.c create mode 100644 src/gtk-list-models/gtkrbtreeprivate.h create mode 100644 src/gtk-list-models/gtksortlistmodel.c create mode 100644 src/gtk-list-models/gtksortlistmodel.h create mode 100644 src/gtk-list-models/meson.build create mode 100644 src/gtk-mount-manager.c create mode 100644 src/gtk-mount-manager.h create mode 100644 src/gtk-mount-prompt.c create mode 100644 src/gtk-mount-prompt.h create mode 100644 src/hks-info.c create mode 100644 src/hks-info.h create mode 100644 src/hks-manager.c create mode 100644 src/hks-manager.h create mode 100644 src/home.c create mode 100644 src/home.h create mode 100644 src/idle-manager.c create mode 100644 src/idle-manager.h create mode 100644 src/keyboard-events.c create mode 100644 src/keyboard-events.h create mode 100644 src/keypad.c create mode 100644 src/keypad.h create mode 100644 src/launcher-entry-manager.c create mode 100644 src/launcher-entry-manager.h create mode 100644 src/layersurface-priv.h create mode 100644 src/layersurface.c create mode 100644 src/layersurface.h create mode 100644 src/layout-manager.c create mode 100644 src/layout-manager.h create mode 100644 src/libphosh-abi.dump create mode 100644 src/libphosh.h create mode 100644 src/libphosh.syms.in create mode 100644 src/location-info.c create mode 100644 src/location-info.h create mode 100644 src/location-manager.c create mode 100644 src/location-manager.h create mode 100644 src/lockscreen-bg.c create mode 100644 src/lockscreen-bg.h create mode 100644 src/lockscreen-manager-priv.h create mode 100644 src/lockscreen-manager.c create mode 100644 src/lockscreen-manager.h create mode 100644 src/lockscreen-priv.h create mode 100644 src/lockscreen.c create mode 100644 src/lockscreen.h create mode 100644 src/lockshield.c create mode 100644 src/lockshield.h create mode 100644 src/main.c create mode 100644 src/manager.c create mode 100644 src/manager.h create mode 100644 src/media-player.c create mode 100644 src/media-player.h create mode 100644 src/meson.build create mode 100644 src/metainfo-cache.c create mode 100644 src/metainfo-cache.h create mode 100644 src/mode-manager.c create mode 100644 src/mode-manager.h create mode 100644 src/monitor-manager.c create mode 100644 src/monitor-manager.h create mode 100644 src/monitor/gamma-table.c create mode 100644 src/monitor/gamma-table.h create mode 100644 src/monitor/head-priv.h create mode 100644 src/monitor/head.c create mode 100644 src/monitor/meson.build create mode 100644 src/monitor/monitor.c create mode 100644 src/monitor/monitor.h create mode 100644 src/mount-manager.c create mode 100644 src/mount-manager.h create mode 100644 src/mount-operation.c create mode 100644 src/mount-operation.h create mode 100644 src/mpris-manager.c create mode 100644 src/mpris-manager.h create mode 100644 src/network-auth-manager.c create mode 100644 src/network-auth-manager.h create mode 100644 src/network-auth-prompt.c create mode 100644 src/network-auth-prompt.h create mode 100644 src/notifications/dbus-notification.c create mode 100644 src/notifications/dbus-notification.h create mode 100644 src/notifications/meson.build create mode 100644 src/notifications/mount-notification.c create mode 100644 src/notifications/mount-notification.h create mode 100644 src/notifications/notification-banner.c create mode 100644 src/notifications/notification-banner.h create mode 100644 src/notifications/notification-content.c create mode 100644 src/notifications/notification-content.h create mode 100644 src/notifications/notification-frame.c create mode 100644 src/notifications/notification-frame.h create mode 100644 src/notifications/notification-list.c create mode 100644 src/notifications/notification-list.h create mode 100644 src/notifications/notification-source.c create mode 100644 src/notifications/notification-source.h create mode 100644 src/notifications/notification.c create mode 100644 src/notifications/notification.h create mode 100644 src/notifications/notify-feedback.c create mode 100644 src/notifications/notify-feedback.h create mode 100644 src/notifications/notify-manager.c create mode 100644 src/notifications/notify-manager.h create mode 100644 src/notifications/timestamp-label-priv.h create mode 100644 src/notifications/timestamp-label.c create mode 100644 src/notifications/timestamp-label.h create mode 100644 src/osd-window.c create mode 100644 src/osd-window.h create mode 100644 src/osk-manager.c create mode 100644 src/osk-manager.h create mode 100644 src/overview.c create mode 100644 src/overview.h create mode 100644 src/password-entry.c create mode 100644 src/password-entry.h create mode 100644 src/phosh-exported-symbols.txt.in create mode 100644 src/phosh-marshalers.list create mode 100644 src/phosh-settings-enums.h create mode 100644 src/phosh-wayland.c create mode 100644 src/phosh-wayland.h create mode 100644 src/phosh.gresources.xml create mode 100644 src/plugin-loader.c create mode 100644 src/plugin-loader.h create mode 100644 src/plugin-shell.h create mode 100644 src/polkit-auth-agent.c create mode 100644 src/polkit-auth-agent.h create mode 100644 src/polkit-auth-prompt.c create mode 100644 src/polkit-auth-prompt.h create mode 100644 src/portal-access-manager.c create mode 100644 src/portal-access-manager.h create mode 100644 src/portal-request.c create mode 100644 src/portal-request.h create mode 100644 src/power-menu-manager.c create mode 100644 src/power-menu-manager.h create mode 100644 src/power-menu.c create mode 100644 src/power-menu.h create mode 100644 src/proximity.c create mode 100644 src/proximity.h create mode 100644 src/quick-setting.c create mode 100644 src/quick-setting.h create mode 100644 src/quick-settings-box.c create mode 100644 src/quick-settings-box.h create mode 100644 src/quick-settings.c create mode 100644 src/quick-settings.h create mode 100644 src/revealer.c create mode 100644 src/revealer.h create mode 100644 src/rotateinfo.c create mode 100644 src/rotateinfo.h create mode 100644 src/rotation-manager.c create mode 100644 src/rotation-manager.h create mode 100644 src/run-command-dialog.c create mode 100644 src/run-command-dialog.h create mode 100644 src/run-command-manager.c create mode 100644 src/run-command-manager.h create mode 100644 src/screen-saver-manager.c create mode 100644 src/screen-saver-manager.h create mode 100644 src/screenshot-manager.c create mode 100644 src/screenshot-manager.h create mode 100644 src/search/meson.build create mode 100644 src/search/search-client.c create mode 100644 src/search/search-client.h create mode 100644 src/search/search-result-meta.c create mode 100644 src/search/search-result-meta.h create mode 100644 src/search/search-source.c create mode 100644 src/search/search-source.h create mode 100644 src/sensor-proxy-manager.c create mode 100644 src/sensor-proxy-manager.h create mode 100644 src/session-manager.c create mode 100644 src/session-manager.h create mode 100644 src/session-presence.c create mode 100644 src/session-presence.h create mode 100644 src/settings.c create mode 100644 src/settings.h create mode 100644 src/settings/audio-device-row.c create mode 100644 src/settings/audio-device-row.h create mode 100644 src/settings/audio-settings.c create mode 100644 src/settings/audio-settings.h create mode 100644 src/settings/channel-bar.c create mode 100644 src/settings/channel-bar.h create mode 100644 src/settings/meson.build create mode 100644 src/shell-priv.h create mode 100644 src/shell.c create mode 100644 src/shell.h create mode 100644 src/splash-manager.c create mode 100644 src/splash-manager.h create mode 100644 src/splash.c create mode 100644 src/splash.h create mode 100644 src/status-icon.c create mode 100644 src/status-icon.h create mode 100644 src/status-page-placeholder.c create mode 100644 src/status-page-placeholder.h create mode 100644 src/status-page.c create mode 100644 src/status-page.h create mode 100644 src/style-manager.c create mode 100644 src/style-manager.h create mode 100644 src/stylesheet/adwaita-dark.css create mode 100644 src/stylesheet/adwaita-hc-light.css create mode 100644 src/stylesheet/common.css create mode 100644 src/suspend-manager.c create mode 100644 src/suspend-manager.h create mode 100644 src/swipe-away-bin.c create mode 100644 src/swipe-away-bin.h create mode 100644 src/system-modal-dialog.c create mode 100644 src/system-modal-dialog.h create mode 100644 src/system-modal.c create mode 100644 src/system-modal.h create mode 100644 src/system-prompt.c create mode 100644 src/system-prompt.h create mode 100644 src/system-prompter.c create mode 100644 src/system-prompter.h create mode 100644 src/thumbnail-priv.h create mode 100644 src/thumbnail.c create mode 100644 src/thumbnail.h create mode 100644 src/top-panel-bg.c create mode 100644 src/top-panel-bg.h create mode 100644 src/top-panel.c create mode 100644 src/top-panel.h create mode 100644 src/toplevel-manager.c create mode 100644 src/toplevel-manager.h create mode 100644 src/toplevel-thumbnail.c create mode 100644 src/toplevel-thumbnail.h create mode 100644 src/toplevel.c create mode 100644 src/toplevel.h create mode 100644 src/torch-info.c create mode 100644 src/torch-info.h create mode 100644 src/torch-manager.c create mode 100644 src/torch-manager.h create mode 100644 src/udev-manager.c create mode 100644 src/udev-manager.h create mode 100644 src/ui/activity.ui create mode 100644 src/ui/app-auth-prompt.ui create mode 100644 src/ui/app-grid-base-button.ui create mode 100644 src/ui/app-grid-button.ui create mode 100644 src/ui/app-grid-folder-button.ui create mode 100644 src/ui/app-grid.ui create mode 100644 src/ui/audio-device-row.ui create mode 100644 src/ui/audio-settings.ui create mode 100644 src/ui/brightness-settings.ui create mode 100644 src/ui/bt-device-row.ui create mode 100644 src/ui/bt-status-page.ui create mode 100644 src/ui/call-notification.ui create mode 100644 src/ui/cell-broadcast-prompt.ui create mode 100644 src/ui/channel-bar.ui create mode 100644 src/ui/emergency-contact-row.ui create mode 100644 src/ui/emergency-menu.ui create mode 100644 src/ui/end-session-dialog.ui create mode 100644 src/ui/feedback-status-page.ui create mode 100644 src/ui/gtk-mount-prompt.ui create mode 100644 src/ui/home.ui create mode 100644 src/ui/keypad.ui create mode 100644 src/ui/lockscreen.ui create mode 100644 src/ui/media-player.ui create mode 100644 src/ui/network-auth-prompt.ui create mode 100644 src/ui/notification-content.ui create mode 100644 src/ui/notification-frame.ui create mode 100644 src/ui/osd-window.ui create mode 100644 src/ui/overview.ui create mode 100644 src/ui/password-entry.ui create mode 100644 src/ui/polkit-auth-prompt.ui create mode 100644 src/ui/power-menu.ui create mode 100644 src/ui/quick-setting.ui create mode 100644 src/ui/quick-settings-box.ui create mode 100644 src/ui/quick-settings.ui create mode 100644 src/ui/revealer.ui create mode 100644 src/ui/run-command-dialog.ui create mode 100644 src/ui/settings.ui create mode 100644 src/ui/splash.ui create mode 100644 src/ui/status-icon.ui create mode 100644 src/ui/status-page-placeholder.ui create mode 100644 src/ui/status-page.ui create mode 100644 src/ui/system-modal-dialog.ui create mode 100644 src/ui/system-prompt.ui create mode 100644 src/ui/timestamp-label.ui create mode 100644 src/ui/top-panel.ui create mode 100644 src/ui/widget-box.ui create mode 100644 src/ui/wifi-network-row.ui create mode 100644 src/ui/wifi-status-page.ui create mode 100644 src/util.c create mode 100644 src/util.h create mode 100644 src/vpn-info.c create mode 100644 src/vpn-info.h create mode 100644 src/vpn-manager.c create mode 100644 src/vpn-manager.h create mode 100644 src/wall-clock-priv.h create mode 100644 src/wall-clock.c create mode 100644 src/wall-clock.h create mode 100644 src/widget-box.c create mode 100644 src/widget-box.h create mode 100644 src/wifi-info.c create mode 100644 src/wifi-info.h create mode 100644 src/wifi-manager.c create mode 100644 src/wifi-manager.h create mode 100644 src/wifi-network-row.c create mode 100644 src/wifi-network-row.h create mode 100644 src/wifi-network.c create mode 100644 src/wifi-network.h create mode 100644 src/wifi-status-page.c create mode 100644 src/wifi-status-page.h create mode 100644 src/wl-buffer.c create mode 100644 src/wl-buffer.h create mode 100644 src/wwan-info.c create mode 100644 src/wwan-info.h create mode 100644 src/wwan/meson.build create mode 100644 src/wwan/phosh-wwan-iface.c create mode 100644 src/wwan/phosh-wwan-iface.h create mode 100644 src/wwan/phosh-wwan-mm.c create mode 100644 src/wwan/phosh-wwan-mm.h create mode 100644 src/wwan/phosh-wwan-ofono.c create mode 100644 src/wwan/phosh-wwan-ofono.h create mode 100644 src/wwan/wwan-manager.c create mode 100644 src/wwan/wwan-manager.h create mode 100644 subprojects/glib.wrap create mode 100644 subprojects/gmobile.wrap create mode 100644 subprojects/gvc.wrap create mode 100644 subprojects/libcall-ui.wrap create mode 100644 subprojects/libfeedback.wrap create mode 100644 subprojects/libhandy.wrap create mode 100644 tests/data/cat.jpg create mode 100644 tests/data/keymap.txt create mode 100644 tests/data/phoc-landscape.ini create mode 100644 tests/data/phoc-notch.ini create mode 100644 tests/data/phosh,tiny.json create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/keyfile create mode 100644 tests/integration/meson.build create mode 100755 tests/integration/run-pytest.in create mode 100755 tests/integration/test_dbus.py create mode 100755 tests/integration/test_output.py create mode 100644 tests/meson.build create mode 100755 tests/mock-mm-nm.py create mode 100644 tests/phosh-logo.png create mode 100644 tests/phosh-test.gresources.xml create mode 100644 tests/screenshot-no-anim-overrides.css create mode 100644 tests/services/meson.build create mode 100644 tests/services/org.gnome.Phosh.MockSearchProvider.service.in create mode 100644 tests/services/testlib-search-provider-app.c create mode 100644 tests/services/testlib-search-provider-app.h create mode 100644 tests/services/testlib-search-provider.c create mode 100644 tests/services/testlib-search-provider.h create mode 100644 tests/stubs/app-tracker.c create mode 100644 tests/stubs/background-manager.c create mode 100644 tests/stubs/bad-instance.h create mode 100644 tests/stubs/bad-prop.h create mode 100644 tests/stubs/evil-icon.c create mode 100644 tests/stubs/evil-icon.h create mode 100644 tests/stubs/lockscreen-manager.c create mode 100644 tests/stubs/meson.build create mode 100644 tests/stubs/phosh.c create mode 100644 tests/stubs/thumbnail.c create mode 100644 tests/stubs/toplevel-manager.c create mode 100644 tests/stubs/toplevel.c create mode 100644 tests/system/config/meson.build create mode 100644 tests/system/meson.build create mode 100644 tests/system/share/applications/demo.app.First.desktop create mode 100644 tests/system/share/desktop-directories/X-Phosh-foo.directory create mode 100644 tests/system/share/desktop-directories/broken.directory create mode 100644 tests/test-activity.c create mode 100644 tests/test-app-auth-prompt.c create mode 100644 tests/test-app-grid-button.c create mode 100644 tests/test-app-grid-folder-button.c create mode 100644 tests/test-app-list-model.c create mode 100644 tests/test-auto-brightness-bucket.c create mode 100644 tests/test-background.c create mode 100644 tests/test-calls-manager.c create mode 100644 tests/test-connectivity-info.c create mode 100644 tests/test-css.c create mode 100644 tests/test-drag-surface.c create mode 100644 tests/test-end-session-dialog.c create mode 100644 tests/test-fading-label.c create mode 100644 tests/test-favourite-model.c create mode 100644 tests/test-folder-info.c create mode 100644 tests/test-gamma-table.c create mode 100644 tests/test-gtk-mount-manager.c create mode 100644 tests/test-head.c create mode 100644 tests/test-idle-manager.c create mode 100644 tests/test-keypad.c create mode 100644 tests/test-layer-surface.c create mode 100644 tests/test-lockscreen.c create mode 100644 tests/test-lockshield.c create mode 100644 tests/test-media-player.c create mode 100644 tests/test-monitor-manager.c create mode 100644 tests/test-mount-notification.c create mode 100644 tests/test-notification-banner.c create mode 100644 tests/test-notification-content.c create mode 100644 tests/test-notification-frame.c create mode 100644 tests/test-notification-list.c create mode 100644 tests/test-notification-source.c create mode 100644 tests/test-notification.c create mode 100644 tests/test-notify-feedback.c create mode 100644 tests/test-notify-manager.c create mode 100644 tests/test-osd-window.c create mode 100644 tests/test-overview.c create mode 100644 tests/test-plugin-loader.c create mode 100644 tests/test-power-menu.c create mode 100644 tests/test-quick-setting.c create mode 100644 tests/test-quick-settings-box.c create mode 100644 tests/test-screenshot-manager.c create mode 100644 tests/test-search-provider.c create mode 100644 tests/test-search-result-meta.c create mode 100644 tests/test-search-source.c create mode 100644 tests/test-shell.c create mode 100644 tests/test-status-icon.c create mode 100644 tests/test-system-modal-dialog.c create mode 100644 tests/test-system-modal.c create mode 100644 tests/test-take-screenshots.c create mode 100644 tests/test-timestamp-label.c create mode 100644 tests/test-util.c create mode 100644 tests/test-wall-clock.c create mode 100644 tests/testlib-calls-mock.c create mode 100644 tests/testlib-calls-mock.h create mode 100644 tests/testlib-compositor.c create mode 100644 tests/testlib-compositor.h create mode 100644 tests/testlib-emergency-calls.c create mode 100644 tests/testlib-emergency-calls.h create mode 100644 tests/testlib-full-shell.c create mode 100644 tests/testlib-full-shell.h create mode 100644 tests/testlib-head-stub.c create mode 100644 tests/testlib-head-stub.h create mode 100644 tests/testlib-mpris-mock.c create mode 100644 tests/testlib-mpris-mock.h create mode 100644 tests/testlib-wait-for-shell-state.c create mode 100644 tests/testlib-wait-for-shell-state.h create mode 100644 tests/testlib.c create mode 100644 tests/testlib.h create mode 100644 tests/user/config-high-contrast/glib-2.0/settings/keyfile create mode 100644 tests/user/config-high-contrast/glib-2.0/settings/meson.build create mode 100644 tests/user/config/meson.build create mode 100644 tests/user/meson.build create mode 100644 tests/user/share/applications/demo.app.Second.desktop create mode 100644 tools/app-buttons.c create mode 100644 tools/app-grid-standalone.c create mode 100644 tools/app-scroll.c create mode 100755 tools/build-symbols-file create mode 100755 tools/check-access-portal create mode 100755 tools/check-deprecated-ui-props create mode 100755 tools/check-end-session create mode 100755 tools/check-exported-symbols create mode 100755 tools/check-license-headers.py create mode 100755 tools/check-mount-operation create mode 100755 tools/check-notification create mode 100755 tools/check-osd create mode 100755 tools/check-screen-saver create mode 100755 tools/check-screenshot create mode 100644 tools/custom-quick-settings-standalone.c create mode 100644 tools/dump-app-list.c create mode 100644 tools/image-notify.c create mode 100644 tools/meson.build create mode 100755 tools/montage-screenshots create mode 100644 tools/notify-blocks.c create mode 100644 tools/notify-server-standalone.c create mode 100644 tools/plugin-prefs-standalone.c create mode 100644 tools/quick-settings-box-standalone.c create mode 100755 tools/run_tool.in create mode 100644 tools/search.c create mode 100755 tools/set_brightness create mode 100644 tools/widget-box-standalone.c diff --git a/.dir-locals.el b/.dir-locals.el new file mode 100644 index 000000000..3a7787474 --- /dev/null +++ b/.dir-locals.el @@ -0,0 +1,22 @@ +( + (c-mode . ( + (c-file-style . "linux") + (indent-tabs-mode . nil) + (c-basic-offset . 2) + (display-fill-column-indicator-column . 100) + )) + (setq auto-mode-alist (cons '("\\.ui$" . nxml-mode) auto-mode-alist)) + (nxml-mode . ( + (indent-tabs-mode . nil) + )) + (css-mode . ( + (css-indent-offset . 2) + )) + (js-mode . ( + (indent-tabs-mode . nil) + (js-indent-level . 2) + )) + (meson-mode . ( + (indent-tabs-mode . nil) + )) +) diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..29f9fcd8c --- /dev/null +++ b/.editorconfig @@ -0,0 +1,29 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[meson.build] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.{c,h,c.in,h.in}] +indent_size = 2 +tab_size = 2 +indent_style = space +max_line_length = 100 + +[*.css] +indent_size = 2 +tab_size = 2 +indent_style = space + +[*.{xml,ui}] +indent_size = 2 +tab_size = 2 +indent_style = space + diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..42b87733a --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +src/libphosh-abi.dump export-ignore gitlab-generated diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..93524f9ba --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +github: phoshmobi +custom: https://ev.phosh.mobi/donate/ diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..994b71025 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +__pycache__/ +_build +TAGS +tags +vgdump +*.swp +*~ +\#*# +.\#* +/subprojects/.wraplock +/subprojects/glib +/subprojects/gmobile +/subprojects/gvc +/subprojects/libcall-ui +/subprojects/libhandy +.vscode/ +*.gcov +debian/phosh.service +debian/files +debian/*.substvars +debian/*debhelper* +debian/gir1.2-phosh-0-dev/ +debian/libphosh-0.45-0/ +debian/libphosh-0.45-dev/ +debian/phosh/ +debian/phosh-common/ +debian/phosh-dev/ +debian/phosh-doc/ +debian/phosh-mobile-tweaks/ +debian/phosh-plugins/ +debian/phosh.postinst +debian/phosh.postrm +debian/phosh.triggers +debian/tmp/ +po/messages.mo +po/missing +po/notexist +po/*.pot diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 000000000..14faf3d18 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,430 @@ +--- +include: + - project: 'guidog/meta-phosh' + ref: '992eb10ac05e479d9cff3dc0ee119a029e298e0c' + file: '/ci/phosh-common-jobs.yml' + - project: 'Infrastructure/freedesktop-ci-templates' + ref: 'd03cddbcff35f26861429fc816c00323f48e99af' + file: '/templates/ci-fairy.yml' + +stages: + - build + - test+docs + - style-checks + - analyze + - package + - deploy + - triggers + +workflow: + rules: + - if: $CI_PIPELINE_SOURCE == 'merge_request_event' + # Don't trigger a branch pipeline if there is an open MR + - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS + when: never + - if: $CI_COMMIT_TAG + - if: $CI_COMMIT_BRANCH + +default: + # Protect CI infra from rogue jobs + timeout: 15 minutes + # Allow jobs to be caneled on new commits + interruptible: true + # Retry on infra hickups automatically + retry: + max: 1 + when: + - 'api_failure' + - 'runner_system_failure' + - 'scheduler_failure' + - 'stuck_or_timeout_failure' + +variables: + # For ci-fairy + FDO_UPSTREAM_REPO: World/Phosh/phosh + # screenshot and asan builds on DEBIAN_IMAGE so keep them in sync + IMAGE_VERSION: v0.0.2025-11-06 + DEBIAN_IMAGE: $CI_REGISTRY/world/phosh/phosh/debian:$IMAGE_VERSION + SCREENSHOT_IMAGE: $CI_REGISTRY/world/phosh/phosh/screenshot:$IMAGE_VERSION + ASAN_IMAGE: $CI_REGISTRY/world/phosh/phosh/asan:$IMAGE_VERSION + XVFB_RUN: xvfb-run -a -s -noreset + COMMON_BUILD_OPTS: -Db_coverage=true --werror -Dtools=true + ALPINE_EDGE_DEPS: alpine-sdk callaudiod-dev elogind-dev evince-dev evolution-data-server-dev feedbackd-dev + gcovr gcr-dev git glib-dev gmobile-dev gnome-bluetooth-dev gnome-desktop-dev + gsettings-desktop-schemas gtk+3.0-dev json-glib-dev libadwaita-dev libgudev-dev + libhandy1-dev libsecret-dev linux-pam-dev meson modemmanager-dev musl-dev networkmanager + networkmanager-dev ninja polkit-elogind-dev pulseaudio-dev qrcodegen-dev ttf-dejavu + upower-dev vala wayland-dev wayland-protocols + RUST_BINDINGS_BRANCH: main + LIBPHOSH: libphosh-0.45 + BUILD_OPTS: -Dcallui-i18n=true -Dphoc_tests=enabled -Dsearchd=true ${COMMON_BUILD_OPTS} + +.test_before_script: &test_before_script + before_script: + - export DEBIAN_FRONTEND=noninteractive + - apt -y update + - apt -y build-dep . + - if [ -n "$SID_PKGS" ]; then + echo "deb http://deb.debian.org/debian/ sid main" >> /etc/apt/sources.list.d/sid.list; + eatmydata apt-get -y update; + apt -y install $SID_PKGS; + fi + +.build_before_script: &build_before_script + before_script: + - export DEBIAN_FRONTEND=noninteractive + - apt -y update + - apt -y build-dep . + - if [ -n "$SID_PKGS" ]; then + echo "deb http://deb.debian.org/debian/ sid main" >> /etc/apt/sources.list.d/sid.list; + eatmydata apt-get -y update; + apt -y install $SID_PKGS; + fi + +.build_step: &build_step + script: + - 'echo "Build opts: ${BUILD_OPTS}"' + - meson setup ${BUILD_OPTS} _build + - meson compile -C _build + +.test_step: &test_step + script: + - export LC_ALL=C.UTF-8 + - meson configure ${BUILD_OPTS} _build + - ${XVFB_RUN} meson test --logbase=tools --print-errorlogs -C _build --suite tools + - ${XVFB_RUN} meson test --logbase=unit --print-errorlogs -C _build --suite unit + - ${XVFB_RUN} meson test --logbase=lint --print-errorlogs -C _build --suite lint + - ${XVFB_RUN} meson test --logbase=integration --print-errorlogs -C _build --suite integration + after_script: + - gcovr --json --gcov-ignore-errors=no_working_dir_found --output=coverage-${CI_JOB_NAME}.json + +# Sanity checks of MR settings and commit logs +commit-log-check: + extends: + - .fdo.ci-fairy + stage: style-checks + variables: + GIT_DEPTH: "100" + needs: [] + script: | + ci-fairy check-commits --signed-off-by --junit-xml=commit-message-junit-report.xml + artifacts: + reports: + junit: commit-message-junit-report.xml + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH' + +build:native-debian-forky: + stage: build + image: ${DEBIAN_IMAGE} + <<: *build_before_script + <<: *build_step + artifacts: + paths: + - _build + +build:lib-debian-forky: + stage: build + image: ${DEBIAN_IMAGE} + variables: + BUILD_OPTS: --werror -Dtests=false -Dtools=false -Dbindings-lib=true -Dvapi=true -Dsearchd=true + <<: *build_before_script + script: + - meson setup ${BUILD_OPTS} _build + - meson compile -C _build + - meson install -C _build + - "gcc -Wall -O2 $(pkgconf --cflags $LIBPHOSH) .gitlab-ci/libphosh-test.c $(pkgconf --libs $LIBPHOSH)" + artifacts: + paths: + - _build + +unit-test:native-debian-forky: + stage: test+docs + image: ${DEBIAN_IMAGE} + needs: + - build:native-debian-forky + <<: *test_before_script + <<: *test_step + artifacts: + when: always + paths: + - _build + - coverage-*.json + - _build/meson-logs/*.junit.xml + reports: + junit: _build/meson-logs/*.junit.xml + +asan-test:native-debian-forky: + stage: test+docs + image: ${ASAN_IMAGE} + tags: [asan] + needs: [] + <<: *test_before_script + script: + - meson setup -Dphoc_tests=enabled -Dtools=false -Db_sanitize=address -Dbuildtype=debug -Dsearchd=true _build-asan + - ${XVFB_RUN} meson test --print-errorlogs -C _build-asan --suite unit --timeout-multiplier 5 + allow_failure: true + +screenshot:native-debian-forky: + stage: test+docs + image: ${SCREENSHOT_IMAGE} + needs: + - build:native-debian-forky + variables: + LC_ALL: C.UTF-8 + G_MESSAGES_DEBUG: all + <<: *test_before_script + script: + # Make sure tests can find the plugins and po files + - "meson install -C _build" + - ${XVFB_RUN} meson test -C _build --print-errorlogs --suite screenshots + - tools/montage-screenshots _build/ + - mv _build/tests/screenshots/* screenshots/ + after_script: + - gcovr --json --gcov-ignore-errors=no_working_dir_found --output=screenshots/coverage-${CI_JOB_NAME}.json + artifacts: + when: always + expose_as: 'Screenshots' + paths: + - screenshots/ + - _build/meson-logs/testlog.txt + - _build/meson-logs/testlog.junit.xml + reports: + junit: _build/meson-logs/testlog.junit.xml + +integration:native-debian-forky: + stage: test+docs + image: ${DEBIAN_IMAGE} + needs: + - build:native-debian-forky + <<: *test_before_script + variables: + SID_PKGS: phoc + # exports + LC_ALL: C.UTF-8 + NO_AT_BRIDGE: 1 + script: + - export TOPSRCDIR="${PWD}" + - export TOPBUILDDIR="${PWD}/_build" + # Make sure tests can find the plugins and po files + - "meson install -C _build" + - ${XVFB_RUN} dbus-run-session _build/tests/integration/run-pytest --junitxml=report-integration-${CI_JOB_NAME}.xml -v tests/integration + after_script: + - gcovr --json --gcov-ignore-errors=no_working_dir_found --output=coverage-integration-${CI_JOB_NAME}.json + artifacts: + paths: + - coverage-integration-${CI_JOB_NAME}.json + - report-integration-${CI_JOB_NAME}.xml + reports: + junit: report-integration-${CI_JOB_NAME}.xml + +build-doc: + stage: test+docs + image: ${DEBIAN_IMAGE} + variables: + BUILD_OPTS: -Dtools=false -Dtests=false -Dgtk_doc=true + <<: *build_before_script + needs: [] + script: + - git clean -dfx + - 'echo "Build opts: ${BUILD_OPTS}"' + - meson setup ${BUILD_OPTS} _build + - meson compile -C _build + - mv _build/docs/phosh-0/ _reference/ + artifacts: + paths: + - _reference + +check-po: + # Checks .ui files for simplification + stage: test+docs + extends: .phosh-check-po + +check-ui: + stage: test+docs + image: ${DEBIAN_IMAGE} + variables: + PHOSH_ADDITIONAL_PKGS: > + libgtk-3-bin + libgtk-4-bin + xvfb + needs: [] + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + paths: + - "**/*.ui" + script: + - !reference [.phosh-prepare-apt] + - | + git remote add target $CI_MERGE_REQUEST_PROJECT_URL.git + echo "Fetching MR target branch $CI_MERGE_REQUEST_TARGET_BRANCH_NAME from $CI_MERGE_REQUEST_PROJECT_URL" + git fetch target "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME" + - ${XVFB_RUN} ./.gitlab-ci/check-ui target/$CI_MERGE_REQUEST_TARGET_BRANCH_NAME $CI_COMMIT_SHA + +check-license-headers: + stage: test+docs + image: ${DEBIAN_IMAGE} + needs: [] + script: + # Checks .c and .h files begin with a license header as + # defined in HACKING.md + - ./tools/check-license-headers.py + +release-tarball: + stage: test+docs + extends: .phosh-check-dist + variables: + MESON_DIST_OPTIONS: --no-tests + +build:native-alpinelinux-edge: + stage: build + image: alpine:edge + allow_failure: true + before_script: + - echo "https://alpine.global.ssl.fastly.net/alpine/edge/testing" >> /etc/apk/repositories + - apk add $ALPINE_EDGE_DEPS + artifacts: + paths: + - _build + script: + - meson setup --werror -Dphoc_tests=disabled _build + - meson compile -C _build + +unit-test:native-alpinelinux-edge: + stage: test+docs + image: alpine:edge + allow_failure: true + needs: + - build:native-alpinelinux-edge + variables: + LC_ALL: C.UTF-8 + before_script: + - echo "https://alpine.global.ssl.fastly.net/alpine/edge/testing" >> /etc/apk/repositories + - apk add xvfb-run $ALPINE_EDGE_DEPS + script: + - ${XVFB_RUN} meson test -C _build --suite unit + artifacts: + when: always + paths: + - _build + when: manual + +build:native-debian-forky-clang: + stage: build + image: ${DEBIAN_IMAGE} + <<: *build_before_script + script: + - CC=clang CC_LD=lld meson setup ${BUILD_OPTS} _build + - meson compile -C _build + artifacts: + paths: + - _build + +check-consistency: + stage: test+docs + extends: .phosh-check-consistency + +check-markdown: + stage: style-checks + extends: .phosh-check-markdown + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + changes: + paths: + - "*.md" + - "docs/*.md" + +check-meson: + stage: style-checks + extends: + - .phosh-check-meson + +check-style: + stage: style-checks + extends: .phosh-format-check + allow_failure: true + rules: + - if: $CI_PIPELINE_SOURCE == "merge_request_event" + +coverage: + image: ${DEBIAN_IMAGE} + stage: analyze + script: + - mkdir coverage-report + - gcovr --add-tracefile 'coverage-*.json' + --add-tracefile 'screenshots/coverage-*.json' + --html-details --print-summary --output coverage-report/index.html + - gcovr --add-tracefile 'coverage-*.json' + --add-tracefile 'screenshots/coverage-*.json' + --xml --output coverage-report/coverage.xml + artifacts: + expose_as: 'Coverage Report' + paths: + - coverage-report + - coverage-report/index.html + reports: + coverage_report: + coverage_format: cobertura + path: coverage-report/coverage.xml + coverage: '/^lines: (\d+\.\d+\%)/' + needs: + - unit-test:native-debian-forky + - screenshot:native-debian-forky + - integration:native-debian-forky + +abi-compliance-checker: + image: ${DEBIAN_IMAGE} + stage: analyze + needs: [build:lib-debian-forky] + <<: *build_before_script + script: + - meson setup --reconfigure -Dabi-check=true _build . + - meson compile -C _build + artifacts: + paths: + - _build/src/libphosh-abi.dump + - _build/src/abi-compliance-check-report.html + when: always + rules: + - if: $CI_COMMIT_TAG + - changes: + paths: + - NEWS + - src/libphosh.syms.in + - when: manual + +package:deb-debian-forky:arm64: + stage: package + variables: + PHOSH_DOCKER_IMAGE: debian:forky + DEB_BUILD_PROFILES: "nodoc nocheck" + DEB_BUILD_OPTIONS: nocheck + extends: .phosh-build-debian-package + tags: + - aarch64 + +pages: + stage: deploy + needs: + - build-doc + script: + - mv _reference/ public/ + artifacts: + paths: + - public + only: + - main + +rust-bindings: + stage: triggers + variables: + UPSTREAM_BRANCH: $CI_COMMIT_REF_NAME + needs: [] + trigger: + project: World/Phosh/libphosh-rs + branch: $RUST_BINDINGS_BRANCH + rules: + - if: $CI_COMMIT_BRANCH == $RUST_BINDINGS_BRANCH + changes: + - src/**/* diff --git a/.gitlab-ci/Makefile b/.gitlab-ci/Makefile new file mode 100644 index 000000000..559d09db8 --- /dev/null +++ b/.gitlab-ci/Makefile @@ -0,0 +1,12 @@ +VERSION=$(shell date --iso) + +all: + sed -i s/v0\..*/v0.0.$(VERSION)/ *.Dockerfile + ./run-docker.sh build --base debian --version 0.0.$(VERSION) + ./run-docker.sh build --base asan --version 0.0.$(VERSION) + ./run-docker.sh build --base screenshot --version 0.0.$(VERSION) + +push: + ./run-docker.sh push --base debian --version 0.0.$(VERSION) + ./run-docker.sh push --base asan --version 0.0.$(VERSION) + ./run-docker.sh push --base screenshot --version 0.0.$(VERSION) diff --git a/.gitlab-ci/README.md b/.gitlab-ci/README.md new file mode 100644 index 000000000..df64a453c --- /dev/null +++ b/.gitlab-ci/README.md @@ -0,0 +1,12 @@ +### Checklist for Updating the Docker Images + + - [ ] Update the `${image}.Dockerfile` file with the dependencies + - [ ] Run `./run-docker.sh build --base ${image} --version ${number}` + - [ ] Run `./run-docker.sh push --base ${image} --version ${number}` + once the Docker image is built; you may need to log in by using + `docker login` or `podman login` like + podman login -u -p registry.gitlab.gnome.org/world/phosh/phosh + See https://docs.gitlab.com/ee/user/packages/container_registry/ + - [ ] Update the `image` keys in the `.gitlab-ci.yml` file with the new + image tag + - [ ] Open a merge request with your changes and let it run diff --git a/.gitlab-ci/asan.Dockerfile b/.gitlab-ci/asan.Dockerfile new file mode 100644 index 000000000..78236cd6e --- /dev/null +++ b/.gitlab-ci/asan.Dockerfile @@ -0,0 +1,10 @@ +FROM registry.gitlab.gnome.org/world/phosh/phosh/debian:v0.0.2025-11-06 + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y update \ + && eatmydata apt-get -y update \ + && cd /home/user/app \ + && echo "deb http://deb.debian.org/debian-debug/ forky-debug main" > /etc/apt/sources.list.d/debug.list \ + && eatmydata apt-get update \ + && eatmydata apt-get -y install libgtk-3-0t64-dbgsym libglib2.0-0t64-dbgsym \ + && eatmydata apt-get clean diff --git a/.gitlab-ci/check-consistency b/.gitlab-ci/check-consistency new file mode 100755 index 000000000..6fd1fb053 --- /dev/null +++ b/.gitlab-ci/check-consistency @@ -0,0 +1,102 @@ +#!/bin/bash +# +# Copyright (C) 2024 The Phosh developers +# SPDX-License-Identifier: GPL-3.0-or-later +# Author: Guido Günther +# +# Check if NEWS, changelog, meson and metainfo are in sync + +set -e + +COLOR= +if [ -n "${TERM}" ] && [ "${TERM}" != "dumb" ]; then + COLOR=1 +fi + +function log +{ + local level="${1}" + local fd=2 + local use_color + + shift + if [ -n "${COLOR}" ]; then + [ "${level}" == warn ] || [ "${level}" == error ] || fd=1 + ! [ -t "${fd}" ] || use_color=1 + + if [ -n "${use_color}" ]; then + case "${level}" in + warn) + tput setaf 1 + ;; + error) + tput bold; tput setaf 1 + ;; + info) + tput setaf 2 + ;; + esac + fi + fi + + echo "$@" + + [ -z "${use_color}" ] || tput sgr0 +} + + +if [ -f debian/changelog ]; then + log info "Fetching version from d/changelog" + VERSION=$(dpkg-parsechangelog -SVersion) +elif [ -f meson.build ]; then + log info "Fetching version from meson build file" + VERSION=$(sed -n "s/.*version\s*:\s*'\([0-9].*\)'.*/\1/p" meson.build) +else + log error "E: Don't know how to get version information" + exit 1 +fi + +echo "I: Checking for '${VERSION}'" + +# News +if ! head -1 NEWS | grep -E -qs "\s+${VERSION}\s*$"; then + log error "E: Version ${VERSION} not in NEWS file" + if [[ "${VERSION}" =~ (~|-)dev ]]; then + log info "I: Unreleased development version, no need to check NEWS" + else + exit 1 + fi +else + log info "I: Found matching news entry" +fi + +# meson.build +MESON_VERSION="${VERSION/\~/.}" +if [ -f meson.build ]; then + if ! grep -qs "version\s*:\s*'$MESON_VERSION'" meson.build; then + log error "E: Version ${MESON_VERSION} not in meson.build file" + exit 1 + else + log info "I: Found matching meson version entry" + fi +else + log info "I: no meson project" +fi + +# appstream info +METAINFO=$(ls data/*metainfo.xml.in* 2>/dev/null || true) +if [ -z "${METAINFO}" ]; then + log warn "W: No metainfo" + exit 0 +fi + +if ! grep -qs "$MESON_VERSION\"" "${METAINFO}"; then + log error "E: Version ${MESON_VERSION} not in metainfo ${METAINFO}" + if [[ "${VERSION}" =~ (~|-)(alpha|beta|dev|rc) ]]; then + log info "I: Not a stable release, no metainfo is fine" + else + exit 1 + fi +else + log info "I: Found matching metainfo entry" +fi diff --git a/.gitlab-ci/check-meson b/.gitlab-ci/check-meson new file mode 100755 index 000000000..5a9c5f09d --- /dev/null +++ b/.gitlab-ci/check-meson @@ -0,0 +1,39 @@ +#!/bin/bash +# +# Copyright (C) 2025 The Phosh developers +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Author: Guido Günther + +ret=0 + +TMPDIR=$(mktemp -d) + +# We check files individually so we can print the diff: +while IFS= read -r -d '' file; do + if ! meson format -e --check-only "$file"; then + echo + echo "Meson file '${file}' needs reformat:" + meson format -e "$file" > "${TMPDIR}/meson.build" + diff -u "$file" "${TMPDIR}/meson.build" + rm -f "${TMPDIR}/meson.build" + ret=1 + fi +done < <(find . -name '*.build' -not -path './subprojects/*' -print0) + +if [ -n "$TMPDIR" ]; then + rmdir "${TMPDIR}" +fi + +if [ $ret -ne 0 ]; then + cat < + +cd po/ || exit 1 +# barf on untranslated C files. Seems intltool +# can't be told to exit with non-zero exit status +# in this case + +if intltool-update -m 2>&1 | grep -E -qs '/.*\.(c|ui|in)'; then + intltool-update -m + exit 1 +fi + +# Check for broken po files +for file in *.po; do + echo -n "Checking ${file}: " + msgfmt -v -c "${file}" + # Check for errors, msgfmt returns 0 on errors too + if msgfmt -c "${file}" 2>&1 | grep -qs 'fatal error'; then + exit 1 + fi +done diff --git a/.gitlab-ci/check-style.py b/.gitlab-ci/check-style.py new file mode 100755 index 000000000..57838660a --- /dev/null +++ b/.gitlab-ci/check-style.py @@ -0,0 +1,185 @@ +#!/bin/env python3 +# +# Based on check-style.py by +# Carlos Garnacho + +import argparse +import os +import re +import subprocess +import sys +import tempfile + +# Path relative to this script +uncrustify_cfg = ".gitlab-ci/uncrustify.cfg" + + +def run_diff(sha): + proc = subprocess.run( + ["git", "diff", "-U0", "--function-context", sha, "HEAD"], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + ) + return proc.stdout.strip().splitlines() + + +def find_chunks(diff): + file_entry_re = re.compile(r"^\+\+\+ b/(.*)$") + diff_chunk_re = re.compile(r"^@@ -\d+,\d+ \+(\d+),(\d+)") + file = None + chunks = [] + + for line in diff: + match = file_entry_re.match(line) + if match: + file = match.group(1) + + match = diff_chunk_re.match(line) + if match: + start = int(match.group(1)) + len = int(match.group(2)) + end = start + len + + if len > 0 and ( + file.endswith(".c") or file.endswith(".h") or file.endswith(".vala") + ): + chunks.append({"file": file, "start": start, "end": end}) + + return chunks + + +def reformat_chunks(chunks, rewrite, dry_run): + # Creates temp file with INDENT-ON/OFF comments + def create_temp_file(file, start, end): + with open(file) as f: + tmp = tempfile.NamedTemporaryFile() + if start > 1: + tmp.write(b"/** *INDENT-OFF* **/\n") + for i, line in enumerate(f, start=1): + if i == start - 1: + tmp.write(b"/** *INDENT-ON* **/\n") + + tmp.write(bytes(line, "utf-8")) + if i == end - 1: + tmp.write(b"/** *INDENT-OFF* **/\n") + tmp.seek(0) + return tmp + + # Removes uncrustify INDENT-ON/OFF helper comments + def remove_indent_comments(output): + tmp = tempfile.NamedTemporaryFile() + for line in output: + if line != b"/** *INDENT-OFF* **/\n" and line != b"/** *INDENT-ON* **/\n": + tmp.write(line) + + tmp.seek(0) + return tmp + + changed = None + for chunk in chunks: + # Add INDENT-ON/OFF comments + tmp = create_temp_file(chunk["file"], chunk["start"], chunk["end"]) + + # uncrustify chunk + proc = subprocess.run( + ["uncrustify", "-c", uncrustify_cfg, "-f", tmp.name], + stdout=subprocess.PIPE, + ) + reindented = proc.stdout.splitlines(keepends=True) + if proc.returncode != 0: + continue + + tmp.close() + + # Remove INDENT-ON/OFF comments + formatted = remove_indent_comments(reindented) + + if dry_run is True: + # Show changes + proc = subprocess.run( + ["diff", "-up", "--color=always", chunk["file"], formatted.name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + ) + diff = proc.stdout + if diff != "": + output = re.sub("\t", "↦\t", diff) + print(output) + changed = True + else: + # Apply changes + diff = subprocess.run( + ["diff", "-up", chunk["file"], formatted.name], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + subprocess.run(["patch", chunk["file"]], input=diff.stdout) + + formatted.close() + + return changed + + +def main(argv): + parser = argparse.ArgumentParser( + description="Check code style. Needs uncrustify installed." + ) + parser.add_argument( + "--sha", metavar="SHA", type=str, help="SHA for the commit to compare HEAD with" + ) + parser.add_argument( + "--dry-run", + "-d", + type=bool, + action=argparse.BooleanOptionalAction, + help="Only print changes to stdout, do not change code", + ) + parser.add_argument( + "--rewrite", + "-r", + type=bool, + action=argparse.BooleanOptionalAction, + help="Whether to amend the result to the last commit (e.g. 'git rebase --exec \"%(prog)s -r\"')", + ) + + if not os.path.exists(".git"): + print("Not in toplevel of a git repository", fille=sys.stderr) + return 1 + + args = parser.parse_args(argv) + sha = args.sha or "HEAD^" + + diff = run_diff(sha) + chunks = find_chunks(diff) + changed = reformat_chunks(chunks, args.rewrite, args.dry_run) + + if args.dry_run is not True and args.rewrite is True: + proc = subprocess.run(["git", "add", "-p"]) + if proc.returncode == 0: + # Commit the added changes as a squash commit + subprocess.run( + ["git", "commit", "--squash", "HEAD", "-C", "HEAD"], + stdout=subprocess.DEVNULL, + ) + # Delete the unapplied changes + subprocess.run(["git", "reset", "--hard"], stdout=subprocess.DEVNULL) + return 0 + elif args.dry_run is True and changed is True: + print( + f""" +Issue the following commands in your local tree to apply the suggested changes: + + $ git rebase {sha} --exec "./.gitlab-ci/check-style.py -r" + $ git rebase --autosquash {sha} + +Don't trust uncrustify unconditionally. +""" + ) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/.gitlab-ci/check-ui b/.gitlab-ci/check-ui new file mode 100755 index 000000000..6d7153e80 --- /dev/null +++ b/.gitlab-ci/check-ui @@ -0,0 +1,72 @@ +#!/bin/bash + +# Copyright (C) 2025 Tether Operations Limited +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Author: Arun Mani J + +# Check the UI files changed from `source_ref` to `target_ref` for +# simplification. +# +# Usage: check-ui target_ref source_ref +# +# By default, `source_ref` is `HEAD` and `target_ref` is `main`. + +set -e + +source="${2:-HEAD}" +target="${1:-main}" + +files=$( + git diff-tree \ + --diff-filter=d \ + --no-commit-id \ + --name-only \ + -r \ + "$target" \ + "$source" \ + -- *.ui +) + +GTK3='' +GTK4='' + +tmpdir=$(mktemp -d) + +status=0 + +for file in $files; do + name=$(basename "$file") + echo "Checking $name" + + if grep -Fq "$GTK3" "$file"; then + tool="gtk-builder-tool" + elif grep -Fq "$GTK4" "$file"; then + tool="gtk4-builder-tool" + else + echo "Could not determine GTK version of $file" + exit 2 + fi + + tmpfile="$tmpdir/$name" + $tool simplify "$file" > "$tmpfile" 2> /dev/null + + if ! cmp --silent "$file" "$tmpfile"; then + echo "$file can be simplified through $tool" + diff -u "$file" "$tmpfile" || : + echo + status=1 + fi + + rm -f "$tmpfile" +done + +rmdir "$tmpdir" + +if [ $status -ne 0 ]; then + echo "Simplify the GTK 3 files using: gtk-builder-tool --simplify --replace file.ui" + echo "Simplify the GTK 4 files using: gtk4-builder-tool --simplify --replace file.ui" +fi + +exit $status diff --git a/.gitlab-ci/commit-rules.yml b/.gitlab-ci/commit-rules.yml new file mode 100644 index 000000000..0125df20b --- /dev/null +++ b/.gitlab-ci/commit-rules.yml @@ -0,0 +1,20 @@ +patterns: + deny: + - regex: '^$CI_MERGE_REQUEST_PROJECT_URL/(-/)?merge_requests/$CI_MERGE_REQUEST_IID$' + message: Commit message must not contain a link to its own merge request + - regex: '^[^:]+: [a-z]' + message: "Commit description in commit message subject should be properly Capitalized. E.g. 'monitor: Avoid crash on unplug'" + where: subject + - regex: '^\S*\.(c|h|ui):' + message: Commit message subject prefix should not include .c, .h etc. + where: subject + - regex: '([^.]\.|[:,;])\s*$' + message: Commit message subject should not end with punctuation + where: subject + - regex: '^[A-Z]\S*:' + message: "Identifier in commit message subject should start lowercase 'monitor: Avoid crash on unplug'" + where: subject + require: + - regex: '^[a-z0-9,\.\+\-/#=_]+:' + message: "Commit message should start with a lowercase identifier 'monitor: Avoid crash on unplug'" + where: subject diff --git a/.gitlab-ci/debian.Dockerfile b/.gitlab-ci/debian.Dockerfile new file mode 100644 index 000000000..e1e2d729a --- /dev/null +++ b/.gitlab-ci/debian.Dockerfile @@ -0,0 +1,17 @@ +FROM debian:forky-slim + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y update \ + && apt-get -y install --no-install-recommends eatmydata \ + && eatmydata apt-get -y dist-upgrade \ + && apt-get -y install --no-install-recommends wget ca-certificates gnupg \ + && eatmydata apt-get --no-install-recommends -y install build-essential git wget gcovr locales uncrustify abi-compliance-checker abi-dumper universal-ctags \ + && eatmydata apt-get -y install clang clang-tools lld \ + && cd /home/user/app \ + && eatmydata apt-get --no-install-recommends -y build-dep . \ + && eatmydata apt-get clean + +# Configure locales for tests +RUN export DEBIAN_FRONTEND=noninteractive && \ + echo 'ar_AE.UTF-8 UTF-8\nde_DE.UTF-8 UTF-8\nen_US.UTF-8 UTF-8\nja_JP.UTF-8 UTF-8\nuk_UA.UTF-8 UTF-8' > /etc/locale.gen && \ + dpkg-reconfigure locales diff --git a/.gitlab-ci/g-style b/.gitlab-ci/g-style new file mode 100755 index 000000000..a37ba0d10 --- /dev/null +++ b/.gitlab-ci/g-style @@ -0,0 +1,32 @@ +#!/bin/bash + +# Copyright (C) 2025 Phosh.mobi e.V. +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +# Author: Gotam Gorabh +# +# Scans specified source directories for 'gchar' usage. +# +# Usage: +# ./g-style src tests plugins +# If no directories are provided, the whole repository is scanned. + +set -e + +# Use command-line arguments if given; if none, scan the whole tree +SOURCE_DIRS=("$@") + +status=0 + +# Search for gchar +if git grep -q "\bgchar\b" -- "${SOURCE_DIRS[@]}"; then + echo "Found 'gchar' in these files:" + git grep -nH "\bgchar\b" -- "${SOURCE_DIRS[@]}" + + echo + echo "gchar usage detected. Please use 'char' over 'gchar'." + status=1 +fi + +exit $status diff --git a/.gitlab-ci/libphosh-test.c b/.gitlab-ci/libphosh-test.c new file mode 100644 index 000000000..31dd99cd9 --- /dev/null +++ b/.gitlab-ci/libphosh-test.c @@ -0,0 +1,13 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include + +int main(void) +{ +} diff --git a/.gitlab-ci/run-docker.sh b/.gitlab-ci/run-docker.sh new file mode 100755 index 000000000..be605bfdd --- /dev/null +++ b/.gitlab-ci/run-docker.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +read_arg() { + # $1 = arg name + # $2 = arg value + # $3 = arg parameter + local rematch='^[^=]*=(.*)$' + if [[ $2 =~ $rematch ]]; then + read "$1" <<< "${BASH_REMATCH[1]}" + else + read "$1" <<< "$3" + # There is no way to shift our callers args, so + # return 1 to indicate they should do it instead. + return 1 + fi +} + +set -e + +build=0 +run=0 +push=0 +list=0 +print_help=0 +no_login=0 + +while (($# > 0)); do + case "${1%%=*}" in + build) build=1;; + run) run=1;; + push) push=1;; + list) list=1;; + help) print_help=1;; + --base|-b) read_arg base "$@" || shift;; + --version|-v) read_arg base_version "$@" || shift;; + --no-login) no_login=1;; + *) echo -e "\e[1;31mERROR\e[0m: Unknown option '$1'"; exit 1;; + esac + shift +done + +if [ $print_help == 1 ]; then + echo "$0 - Build and run Docker images" + echo "" + echo "Usage: $0 [options] [basename]" + echo "" + echo "Available commands" + echo "" + echo " build --base= - Build Docker image .Dockerfile" + echo " run --base= - Run Docker image " + echo " push --base= - Push Docker image to the registry" + echo " list - List available images" + echo " help - This help message" + echo "" + exit 0 +fi + +cd "$(dirname "$0")" + +if [ $list == 1 ]; then + echo "Available Docker images:" + for f in *.Dockerfile; do + filename=$( basename -- "$f" ) + basename="${filename%.*}" + + echo -e " \e[1;39m$basename\e[0m" + done + exit 0 +fi + +# All commands after this require --base to be set +if [ -z $base ]; then + echo "Usage: $0 " + exit 1 +fi + +if [ ! -f "$base.Dockerfile" ]; then + echo -e "\e[1;31mERROR\e[0m: Dockerfile for '$base' not found" + exit 1 +fi + +if [ -z $base_version ]; then + base_version="latest" +elif [ $base_version != "latest" ]; then + base_version="v$base_version" +fi + +if [ ! -x "$(command -v docker)" ] || docker --help |& grep -q podman; then + # Docker is actually implemented by podman, and its OCI output + # is incompatible with some of the dockerd instances on GitLab + # CI runners. + echo "Using: Podman" + format="--format docker" + CMD="podman" +else + echo "Using: Docker" + format="" + CMD="sudo docker" +fi + +REGISTRY="registry.gitlab.gnome.org" +REPO="world/phosh/phosh" +TAG="${REGISTRY}/${REPO}/${base}:${base_version}" + +if [ $build == 1 ]; then + echo -e "\e[1;32mBUILDING\e[0m: ${base} as ${TAG}" + ${CMD} build \ + ${format} \ + --no-cache \ + --volume "$(pwd)/..:/home/user/app" \ + --build-arg HOST_USER_ID="$UID" \ + --tag "${TAG}" \ + --file "${base}.Dockerfile" . + exit $? +fi + +if [ $push == 1 ]; then + echo -e "\e[1;32mPUSHING\e[0m: ${base} as ${TAG}" + + if [ $no_login == 0 ]; then + ${CMD} login ${REGISTRY} + fi + + ${CMD} push ${TAG} + exit $? +fi + +if [ $run == 1 ]; then + echo -e "\e[1;32mRUNNING\e[0m: ${base} as ${TAG}" + ${CMD} run \ + --cap-add NET_ADMIN,SYS_PTRACE \ + --rm \ + --volume "$(pwd)/..:/home/user/app" \ + --workdir "/home/user/app" \ + --tty \ + --interactive "${TAG}" \ + bash + exit $? +fi diff --git a/.gitlab-ci/screenshot.Dockerfile b/.gitlab-ci/screenshot.Dockerfile new file mode 100644 index 000000000..824c6c95b --- /dev/null +++ b/.gitlab-ci/screenshot.Dockerfile @@ -0,0 +1,15 @@ +FROM registry.gitlab.gnome.org/world/phosh/phosh/debian:v0.0.2025-11-06 + +RUN export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y update \ + && eatmydata apt-get -y update \ + && cd /home/user/app \ + && eatmydata apt-get -y -f install \ + && eatmydata apt-get -y install imagemagick-7.q16 fonts-lato fonts-vlgothic gnome-shell-common gsettings-desktop-schemas feedbackd fonts-cantarell librsvg2-common \ + && eatmydata apt-get -y install python3 python3-junit.xml python3-coloredlogs \ + && eatmydata apt-get clean + +# Allow translation files so GTK 3 can set widget direction (not needed in GTK 4) +RUN sed -i "s@path-exclude /usr/share/locale/\*@# path-exclude /usr/share/locale/\*@" /etc/dpkg/dpkg.cfg.d/docker +# Reinstall GTK 3 so translation files are setup +RUN eatmydata apt-get reinstall -y libgtk-3-common diff --git a/.gitlab-ci/uncrustify.cfg b/.gitlab-ci/uncrustify.cfg new file mode 100644 index 000000000..98a3a814c --- /dev/null +++ b/.gitlab-ci/uncrustify.cfg @@ -0,0 +1,148 @@ +# +# Uncrustify config for phosh and related projects +# + +# A span is the number of lines considered +# A threshold is the maximum number of columns an item is moved + +# Indent by two spaces +indent_columns = 2 +# No tabs +indent_with_tabs = 0 +# Line length +code_width = 100 +# Whether to remove superfluous semicolons +mod_remove_extra_semicolon = true +# indent goto by 1 (or -1 brace level) +indent_label = -1 +# don't indent case after switch +indent_switch_case = 0 + +# +# Keywords and operators +# +# Add between 'do' and '{'. +sp_do_brace_open = add +# Add space between '}' and 'while'. +sp_brace_close_while = add +# Add 'while' and '('. +sp_while_paren_open = add +# Add or remove space around boolean operators '&&' and '||'. +sp_bool = add +# Ternary operator +sp_cond_ternary_short = remove +# Remove newline between 'struct and '{'. +nl_struct_brace = remove +# Remove newline between 'if' and '{'. +nl_if_brace = remove +# Remove newline between '}' and 'else'. +nl_brace_else = remove +# Remove newline between 'else' and '{'. +nl_else_brace = remove +# Remove newline between 'else' and 'if'. +nl_else_if = remove +# Add or remove newline between 'for' and '{'. +nl_for_brace = remove +# Add or remove newline between 'while' and '{'. +nl_while_brace = remove +# Treat iterators as for loops: +set FOR wl_list_for_each wl_list_for_each_reverse wl_list_for_each_safe +# Remove braces on single line if/for/while statements +mod_full_brace_if = remove +mod_full_brace_for = remove +mod_full_brace_while = remove +# If any must be braced, they are all braced. If all can be unbraced, then the braces are removed. +mod_full_brace_if_chain = 1 +# Remove braces around case (when there are no variables declarations) +mod_case_brace = remove +# Don't remove branches if the statement has more than one line +mod_full_brace_nl = 2 + +# +# Function declarations, definitions and calls +# +# Add space between function name and '(' on function declaration. +sp_func_proto_paren = add +# Add or remove space between function name and '()' on function declaration +# without parameters. +sp_func_proto_paren_empty = add +# Add space between function name and '(' on function definition. +sp_func_def_paren = add +# Add or remove space between function name and '(' on function calls. +sp_func_call_paren = add +# Specialcase i18n macros +set func_call_user _ N_ C_ +sp_func_call_user_paren = remove + +# Whether to force indentation of function definitions to start in column 1. +indent_func_def_force_col1 = true +# Add newline between return type and function name in a function definition. +nl_func_type_name = add +# Add newline between function signature and '{'. +nl_fdef_brace = add +# Whether to align variable definitions in prototypes and functions. +align_func_params = true +# The span for aligning function prototypes. +align_func_proto_span = 8 +# Add space between 'decltype(...)' and word. +sp_after_decltype = add +# Add or remove space after a pointer star '*', if followed by a function +# prototype or function definition. +sp_after_ptr_star_func = remove +# Add or remove newline between a function call's ')' and '{', as in +# 'list_for_each(item, &list) { }'. +nl_fcall_brace = add + +# +# Typedefs +# +# Add space between '}' and the name of a typedef on the same line. +sp_brace_typedef = add + +# +# Comments +# +# Add space after the opening of a C++ comment, i.e. '// A' vs. '//A'. +sp_cmt_cpp_start = add + +# +# Preprocessor +# +# Add or remove space between #else or #endif and a trailing comment. +sp_endif_cmt = add +# Offset value that controls the indentation of the body of a multiline #define +pp_multiline_define_body_indent = 2 + +# Newlines at the start and end of the file. +nl_start_of_file = remove +nl_end_of_file = add +nl_end_of_file_min = 1 + +# +# Variable definitions +# +# How to align the '*' in variable definitions. +# +# 0: Part of the type 'void * foo;' (default) +# 1: Part of the variable 'void *foo;' +# 2: Dangling 'void *foo;' +# Dangling: the '*' will not be taken into account when aligning. +align_var_def_star_style = 2 +# Same for typedefs +align_typedef_star_style = 2 +# The gap for aligning struct/union member definitions. +align_var_struct_gap = 1 +# The span for aligning struct/union member definitions. +align_var_struct_span = 4 +# The threshold for aligning struct/union member definitions. +align_var_struct_thresh = 8 + +# Remove space between pointer stars '*'. +sp_between_ptr_star = remove +# Add space before '(' of control statements ('if', 'for', 'switch', 'while', etc.) +sp_before_sparen = add + +# Add spaces around assignments and arithmethic operators +sp_assign = add +sp_arith = add + diff --git a/.gitlab/issue_templates/bug.md b/.gitlab/issue_templates/bug.md new file mode 100644 index 000000000..351137971 --- /dev/null +++ b/.gitlab/issue_templates/bug.md @@ -0,0 +1,93 @@ +# What problem did you encounter? + +Please describe the problem in general terms. + +## How to reproduce? + +Please provide steps to reproduce the issue. If it's a graphical issue please +attach screenshot. + +## What is the (wrong) result? + +## What is the expected behaviour? + +# Expectations + +If you're unsure about the component that triggers this bug please +check in our Matrix channel or with your distribution upfront to +clarify. If the issue is in a closely related software component we +can reassign the issue but if it's in a component "further away" you +might otherwise need to refile the issue as the project might reside +in a completely different issue tracker. + +Note that we only support the latest upstream version, see +[our releases](https://phosh.mobi/releases). If you report an issue in +an older version it will get closed unless it is obvious to the +developers that the issue is still present in the current version. + +If your device is running Halium (Droidian, FuriOS) please ensure with +your distribution first that the issue isn't triggered by a downstrem +modification. + +# Context + +Please describe your setup, or write additional information relevant +to this bug. Here, you can also write why this is actually a bug, +if that might not be immediately clear to the reader. + +# Which version did you encounter the bug in? + +- [ ] I used a distribution package. Please paste the output of + ``phosh-session --version`` below. + +- [ ] I compiled it myself. If you compiled phosh from source please provide the + git revision e.g. by running ``git log -1 --pretty=oneline`` and pasting + the output below. + +``` + Phosh Version: +``` + +# How are you running phosh? + +## Device + +- [ ] On a mobile phone. Please specify the model (e.g. PinePhone, OnePlus 6, Librem 5): +- [ ] On a tablet/convertible. Please specify the model (e.g. IdeaPad Duet): +- [ ] In a nested session. Please specify the command you start phosh with: +- [ ] As a VM image (e.g. [Phosh Weekly images][]). Please elaborate: +- [ ] Something else. Please elaborate: + +## Distribution + +Which Linux distribution are you using? If postmarketOS please also specify +if you're using systemd or OpenRC: + +## How is Phosh started on your system + +- [ ] Through a display manager + + - [ ] phrog + - [ ] gdm + - [ ] lightdm + - [ ] other: + +- [ ] other (e.g. systemd unit, please elaborate): + +# Relevant logfiles + +Please provide relevant logs. You can get the logs since last boot +with ``journalctl -b 0`` and `journalctl --user -b 0`. + +See `phosh(1)` on debugging related environment variables and `phosh-session(1)` +on where you can set them. + +# System information + +Please attach the output of + +``` +phosh-mobile-settings --debug-info +``` + +[Phosh Weekly Image]: https://images.phosh.mobi/nightly/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 000000000..e69de29bb diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 000000000..71de86823 --- /dev/null +++ b/.ruff.toml @@ -0,0 +1 @@ +line-length = 100 diff --git a/COPYING b/COPYING new file mode 100644 index 000000000..94a9ed024 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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 3 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, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/HACKING.md b/HACKING.md new file mode 100644 index 000000000..e6e49519a --- /dev/null +++ b/HACKING.md @@ -0,0 +1,595 @@ +# Contributing to Phosh + +Below are some basic guidelines on coding style, source file layout and merge +requests that hopefully makes it easier for you to land your code. + +## Building + +For build instructions see the [README.md](./README.md) + +## Development Documentation + +For internal API documentation as well as notes for application developers see [here](https://world.pages.gitlab.gnome.org/Phosh/phosh/). + +## Merge requests + +Before filing a pull request run the tests: + +```sh +meson test -C _build --print-errorlogs +``` + +Use descriptive commit messages, see + + + +and check + + + +for good examples. The commits in a merge request should have "recipe" +style history rather than being a work log. See +[here](https://www.bitsnbites.eu/git-history-work-log-vs-recipe/) for +an explanation of the difference. The advantage is that the code stays +bisectable and individual bits can be cherry-picked or reverted. + +### Checklist + +When submitting a merge request consider checking these first: + +- [ ] Does the code use the below coding patterns? +- [ ] Is the commit history in recipe style (see above)? +- [ ] Do the commit messages reference the bugs they fix. If so, + - Use `Helps:` if the commit partially addresses a bug or contributes + to a solution. + - Use `Closes:` if the commit fully resolves the bug. This allows the + release script to detect & mention it in `NEWS` file. +- [ ] Does the code crash or introduce new `CRITICAL` or `WARNING` + messages in the log or when run form the console. If so, fix + these first? +- [ ] Is the new code covered by any tests? This could be a unit test, + an added [screenshot test](./tests/test-take-screenshots.c), + a tool to exercise new DBus API (see e.g. + [tools/check-mount-operation](./tools/check-mount-operation). +- [ ] Are property assignments to default values removed from UI files? (See + `gtk-builder-tool simplify file.ui`) + +If any of the above criteria aren't met yet it's still fine (and +encouraged) to open a merge request marked as draft. Please indicate +why you consider it draft in this case. As Phosh is used on a wide +range of devices and distributions please indicate in what scenarios +you tested your code. + +## Coding Patterns + +### Coding Style + +We're mostly using [libhandy's Coding Style][1]. + +These are the differences: + +- We're not picky about GTK style function argument indentation, that is + having multiple arguments on one line is also o.k. +- For signal handler callbacks we prefer for the `on_` naming + pattern e.g. `on_clicked` since this helps to keep the namespace clean. See + below. +- Since we're not a library we usually use `G_DEFINE_TYPE` instead of + `G_DEFINE_TYPE_WITH_PRIVATE` (except when we need a deriveable + type) since it makes the rest of the code more compact. + +### Source file layout + +We use one file per GObject. It should be named like the GObject without +the phosh prefix, lowercase and '\_' replaced by '-'. So a hypothetical +`PhoshThing` would go to `src/thing.c`. If there are likely name +clashes add the `phosh-` prefix (e.g. `phosh-wayland.c`). The +individual C files should be structured as (top to bottom of file): + +- License boilerplate + + ```c + /* + * Copyright (C) year copyright holder + * + * SPDX-License-Identifier: GPL-3-or-later + * Author: you + */ + ``` + +- A log domain, usually the filename with `phosh-` prefix + + ```c + #define G_LOG_DOMAIN "phosh-thing" + ``` + +- `#include`s: + Phosh ones go first, then glib/gtk, then generic C headers. These blocks + are separated by newline and each sorted alphabetically: + + ``` + #define G_LOG_DOMAIN "phosh-settings" + + #include "phosh-config.h" + + #include "settings.h" + #include "shell-priv.h" + + #include + #include + + #include + ``` + + This helps to detect missing headers in includes. + +- docstring: + If you have trouble to describe the class concisely, then it might be an indication + that it should be split into multiple classes. + + ```c + /** + * PhoshYourThing: + * + * Short, single line, summary + * + * A longer description with details that can be + * multiline. + * + * Since: 0.44.0 + */ + ``` + +- property enum + + ```c + enum { + PROP_0, + PROP_FOO, + PROP_BAR, + LAST_PROP + }; + static GParamSpec *props[LAST_PROP]; + ``` + +- signal enum + + ```c + enum { + FOO_HAPPENED, + BAR_TRIGGERED, + N_SIGNALS + }; + static guint signals[N_SIGNALS]; + ``` + +- type definitions + + ```c + typedef struct _PhoshThing { + GObject parent; + + ... + } PhoshThing; + + G_DEFINE_TYPE (PhoshThing, phosh_thing, G_TYPE_OBJECT) + ``` + +- private methods and callbacks (these can also go at convenient + places above `phosh_thing_constructed ()`) +- `phosh_thing_set_property ()`. Set properties. + If setting a property requires more than a single line prefer adding a setter method, + e.g. for the `foo` property, the setter method would be + `phosh_thing_set_foo ()`. This is almost always the case as we prefer + `G_PARAM_EXPLICIT_NOTIFY` (see below). + + ```c + static void + phosh_thing_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) + { + PhoshThing *self = PHOSH_TING (object); + + switch (property_id) { + case PROP_FOO: + phosh_thing_set_foo (self, g_value_get_string (value)); + break; + … + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } + } + ``` + +- `phosh_thing_get_property ()` +- `phosh_thing_constructed ()`: Finish object constructions. Usually only + needed if you need the values of multiple properties passed at object + construction time. +- `phosh_thing_dispose ()`: Usually only needed when you need to break + reference cycles. Otherwise prefer `finalize`. As `dispose` can be run + multiple times use `g_clear_*` to avoid freeing resources multiple times: + + ```c + static void + phosh_thing_dispose (GObject *object) + { + PhoshThing *self = PHOSH_THING (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->bar); + g_clear_pointer (&self->foo, g_free); + + G_OBJECT_CLASS (phosh_thing_parent_class)->dispose (object); + } + ``` + +- `phosh_thing_finalize ()`: Free allocated resources. +- `phosh_thing_class_init ()`: Define properties and signals. For widget + templates bind child widgets and signal handlers. +- `phosh_thing_init ()`: Initialize defaults for member variables here. +- `phosh_thing_new ()`: A convenience wrapper around `g_object_new ()`. Don't + do further object initialization here but rather do that in + `phosh_thing_init ()`, `phosh_thing_constructed ()` or individual property + setters. This ensures that objects can be constructed either via this + constructor or `g_object_new ()`. +- Public methods, all starting with the object name (i.e. `phosh_thing_`). + +The reason public methods go at the bottom is that they have +declarations in the header file and can thus be referenced from +anywhere else in the source file. + +### Derivable Parent Widgets + +If the widget is derivable and accepts a child, then prefer to expose the child +as a property than as its UI child (`` in UI file). This way we align +with how GTK 4 structures the API and avoid the need to hijack the parent +container's add/remove method in the derivatives. + +For example, a derivable parent widget called `PhoshFoo` would have the code for +adding and removing child like this: + +```c +... + +enum { + PROP_0, + PROP_CHILD, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct { + GtkWidget *child; +} PhoshFooPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshFoo, phosh_foo, GTK_TYPE_BOX); + +... + +static void +phosh_foo_set_child (PhoshFoo *self, GtkWidget *child) +{ + PhoshFooPrivate *priv; + + g_return_if_fail (PHOSH_IS_FOO (self)); + + priv = phosh_foo_get_instance_private (self); + + if (priv->child) { + /* Remove the existing child */ + } + + priv->child = child; + + /* child can be NULL, which is used to remove + existing child without replacement. */ + if (priv->child) { + /* Add the new child */ + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]); +} +``` + +For the widgets that take multiple children and adds them to internal widgets, +prefer to implement `GtkBuildable` interface and expose methods to add and +remove child like `phosh_foo_add` and `phosh_foo_remove`. + +As an example, a `PhoshFoo` that can have multiple children, its code would be +like: + +```c +... + +static void phosh_foo_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshFoo, phosh_foo, + PHOSH_TYPE_FOO, + G_ADD_PRIVATE (PhoshFoo) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + phosh_foo_buildable_init)) + +... + +static GtkBuildableIface *parent_buildable_iface; + +static void +phosh_foo_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const gchar *type) +{ + PhoshFoo *self = PHOSH_FOO (buildable); + + /* Check if the child is meant for PhoshFoo by checking child's type etc. */ + if (condition) { + phosh_foo_add (self, child); + return; + } + + /* The parent is a container itself so chain up */ + parent_buildable_iface->add_child (buildable, builder, child, type); +} + + +static void +phosh_foo_buildable_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + iface->add_child = phosh_foo_buildable_add_child; +} + +... + +void +phosh_foo_add (PhoshFoo *self, GtkWidget *child) +{ + /* Add the child to self using usual logic. */ + ... +} +``` + +The ultimate aim is to do a little heavy-lifting in the widget implementations, +so it is easy to port them to GTK 4. And, as a direct consequence, restrict the +usage of `GtkContainer` APIs for implementation logic only and expose public +methods to do the same. + +### CSS Theming + +For custom widgets set the css name using `gtk_widget_class_set_css_name ()`. +There's no need set an (additional) style class in the ui file. + +*Good*: + +```c +static void +phosh_lockscreen_class_init (PhoshLockscreenClass *klass) +{ + … + gtk_widget_class_set_css_name (widget_class, "phosh-lockscreen"); + … +} +``` + +*Bad*: + +```xml + +``` + +### Properties + +#### Signal emission on changed properties + +Except for `G_CONSTRUCT_ONLY` properties use `G_PARAM_EXPLICIT_NOTIFY` and notify +about property changes only when the underlying variable changes value: + +```c +static void +on_present_changed (PhoshDockedInfo *self) +{ + … + + if (self->enabled == enabled) + return; + + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + +static void +phosh_docked_info_class_init (PhoshDockedInfoClass *klass) +{ + … + + /** + * PhoshDockedInfo:enabled: + * + * Whether docked mode is enabled + * + * Since: 0.44.0 + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + … +} +``` + +This makes sure we minimize the notifications on changed property values. + +#### Property documentation + +Prefer a docstring over filling in the properties' `nick` and `blurb`. See +example above. + +#### Property bindings + +If the state of a property depends on the state of another one prefer +`g_object_bind_property ()` to keep these in sync: + +*Good*: + +```c + g_object_bind_property (self, "bar", + self->avatar, "loadable-icon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +``` + +This has the upside that the binding goes away automatically when +either of the two objects gets disposed and also avoids bugs in manual +syncing code. + +For widgets you can construct the binding via the UI XML: + +```xml + + + … + +``` + +### Signals + +Signals should be documented. Since `class_offset`, `accumulator` and +`c_marshaller` are often unused we put them on a single line. + +*Good*: + +```C + /** + * PhoshWwanManager::new-cbm + * @self: The wwan manager + * @message: The message text + * @channel: The channel specifying the source of the CBM + * + * This signal is emitted when a new cell broadcast message is + * received. + * + * Since: 0.44.0 + */ + signals[NEW_CBM] = g_signal_new ("new-cbm", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_UINT); +``` + +In the same source file prefer `g_signal_emit` over +`g_signal_emit_by_name`: + +*Good*: + +```C + g_signal_emit (self, signals[NEW_CBM], "data", 32); +``` + +### Callbacks + +There's callbacks for signals, async functions, and actions. We +usually have them all start with `on_` to make it easy to spot +that these aren't just methods (and hence have restrictions regarding +their function arguments and return values). + +- Signal handlers are named `on_`. E.g. the handler for a + `GListModel`s `items-changed` signal would be named + `on_items_changed ()`. To avoid ambiguity one can add the emitter's + name (`on_devices_list_box_items_changed ()`). + +- For `notify::` signal handler callbacks acting on property changes + we use the `on__changed ()` naming. E.g. + `on_nm_signal_strength_changed ()`. + +- For `GAsyncReadyCallback`s we use `on__ready()`, e.g. + the callback for `nm_client_new_async` that invokes `nm_client_new_finish` + would be named `on_nm_client_new_ready ()`. + +- For actions we use `on__activated`, e.g. for a `go-back` action + we'd use `on_go_back_activated ()`. + +- In cases where the signal handler name should express what it does + rather than what signal it connects to, we use a `_cb` suffix. This + is often the case when we want to use the same signal handler to + handle multiple signals. E.g. a callback that updates a + `GtkStackPage` when a signal happens would be named + `update_stack_page_cb ()`. + +### API contracts + +Public (non static) functions must check the input arguments at the +top of the function. This makes it easy to reuse them in other parts +and makes API misuse easy to debug via `G_DEBUG=fatal-criticals`. You +usually want to check argument types and if the arguments fulfill the +requirements (e.g. if they need to be non-NULL). Public functions +should have doc strings. + +*Good*: + +```c +/** + * phosh_foo_set_name: + * @self: The foo + * @name: The name to set + * + * Set Foo's `name` + */ +void +phosh_foo_set_name (PhoshFoo *self, const char *name) +{ + GtkWidget *somewidget; + + g_return_if_fail (PHOSH_IS_FOO (self)); + g_return_if_fail (!name); + + … +} +``` + +#### GObject introspection annotations + +For the Rust bindings we want to have introspection annotations on public methods. +Use a space after the colon: + +*Good*: + +```C + * Returns: (transfer none): The generated wisdom +``` + +*Bad*: + +```C + * Returns:(transfer none): The generated wisdom +``` + +## Public API + +Phosh's lockscreen and quick setting plugins can use the ABI provided by the +[plugins symbols file][]. + +Phosh also provides a shared library to run the "shell in a box" to be +e.g. used by greeters. The ABI available to library users is the +plugins ABI plus the symbols from the [library symbols file][]. If you +need a new symbol for the library but not the plugins, consider adding +it there. + +Symbols in these files can only be changed in a backward compatible manner or +we need to bump the library API version. + +[1]: https://gitlab.gnome.org/GNOME/libhandy/blob/master/HACKING.md#coding-style +[plugins symbols file]: src/phosh-exported-symbols.txt.in +[library symbols file]: src/libphosh.syms.in +[public API documentation]: https://world.pages.gitlab.gnome.org/Phosh/phosh diff --git a/NEWS b/NEWS new file mode 100644 index 000000000..b897a6d09 --- /dev/null +++ b/NEWS @@ -0,0 +1,1479 @@ +phosh 0.53.0 +------------ +Released February 2026 +* Various auto brightness improvements, e.g. + * remember auto brightness offset + * compensate for night light +* overview: Show thumbnails for launching apps +* overview: Style improvements +* polkit-prompt: Show more user data +* Activation fixes for notifications + (Needs https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/9269) +* caffeine: Allow to open plugin prefs from the status page +* Multiple other fixes (Use alias for Bluetooth devices, improve app icon + lookup, …) +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/647 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/813 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1229 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1280 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1287 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1292 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1297 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1300 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1298 +* Contributors: + * AJ Dunevent + * Arun Mani J + * Guido Günther + * Rudra Pratap Singh +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Álvaro Burns (pt_BR) + * Danial Behzadi (fa) + * Efstathios Iosifidis (el) + * Ekaterine Papava (ka) + * Flynn Peck (kw) + * Fredrik Fyksen (nb) + * Jürgen Benvenuti (de) + * Martin (sl) + * Quentin PAGÈS (oc) + * Sabri Ünal (tr) + * Yuri Chornoivan (uk) + * twlvnn kraftwerk (bg) + +phosh 0.52.1 +------------ +Released January 2026 +* Prevent crash in gvc with newer pipewire 1.5.84 / wireplumber 0.5.13 +* Contributors: + * Guido Günther + +phosh 0.52.0 +------------ +Released January 2026 +* Add `DebugControl` debug interface to control debug options at runtime +* Add brightness gesture to lockscreen +* Wi-Fi hotspot: Show QR code to ease connecting devices +* Bug fixes and internal cleanups +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/814 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1259 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1285 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1286 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1293 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1294 +* Contributors: + * Achill Gilgenast + * Arun Mani J + * Guido Günther + * Rudra Pratap Singh +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Asier Saratsua Garmendia (eu) + * Danial Behzadi (fa) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Jiri Grönroos (fi) + * Juliano de Souza Camargo (pt_BR) + * Jürgen Benvenuti (de) + * Martin (sl) + * Rafael Fontenelle (pt_BR) + * Victor Dargallo (ca) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.51.0 +------------ +Released November 2025 +* Add location quick setting +* caffeine: Allow to select duration and configure durations via mobile + settings +* brightness/auto-brightness improvements: + * Handle auto-brightness via a bucket based algorithm + * Let brightness slider be an offset around the auto-brightness value + * Icon next to brightness slider indicates auto-brightness on/off + * Gradually transition to new auto-brightness value + * Auto-brightness can be enabled/disabled via a status page +* torch: Allow to configure minimal brightness via hwdb/udev +* Fix upcoming-event’s plugin empty state +* Fix “Open settings” button position on devices with rounded corners +* Lots of internal cleanups and fixes +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1255 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1272 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1274 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1276 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1281 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1282 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/783 +* Contributors: + * Arun Mani J + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * Robert Eckelmann + * Rudra Pratap Singh +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Efstathios Iosifidis (el) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Juliano de Souza Camargo (pt_BR) + * Martin (sl) + * Nathan Follens (nl) + * Sabri Ünal (tr) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.50.0 +------------ +Released October 2025 +* Allow to open the top-bar settings panel easily +* Move brightness handling to phosh, it got dropped from g-s-d 49. +* Mark demo plugins as `NoDisplay` so they don't show in p-m-s +* Don't crash g-c-c when running nested +* Reset them when auto-HighContrast is turned off +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1261 +* Contributors: + * Anna (cybertailor) Vyalkova + * Evangelos Ribeiro Tzaras + * Guido Günther +* UI translations: + * Emin Tufan Çetin (tr) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Kristjan SCHMIDT (eo) + * Nathan Follens (nl) + * Philipp Kiemle (de) + * Sabri Ünal (tr) + * twlvnn kraftwerk (bg) + * Yaron Shahrabani (he) + +phosh 0.49.0 +------------ +Released August 2025 +* upcoming-events: Allow to exclude days without events +* Use phrosh portal when available +* overview: Don't let search entry take the whole width +* Improve media widget's cover art thumbnails +* Improve keypad and search bar style +* Cellbroadcast related fixes +* Add searchd +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1242 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1245 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/290 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/703 +* Contributors: + * Achill Gilgenast + * Arun Mani J + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * hustlerone + * Rudra Pratap Singh + * Zander Brown +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Ekaterine Papava (ka) + * Juliano de Souza Camargo (pt_BR) + * Martin (sl) + * Yuri Chornoivan (uk) + * twlvnn kraftwerk (bg) + +phosh 0.48.0 +------------ +Released June 2025 +* New lockscreen plugin that allows to interact with all currently + running media players that use the MPRIS protocol. +* Build VAPI files so one can also write plugins in vala (See + https://gitlab.gnome.org/guidog/phosh-vala-plugins) for an example +* Enable Cell Broadcast dialogs by default. Mobile Settings has the + needed UI bits now. +* Update list of adaptive apps +* Various bug fixes and GTK4 preps +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1234 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1237 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1154 +* Contributors: + * Arun Mani J + * fossdd + * Guido Günther +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Martin (sl) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + * Álvaro Burns (pt_BR) + +phosh 0.47.0 +------------ +Released May 2025 +* notifications: Handle cellbroacast events from the desktop portal v2 + spec +* app-list-model: Track StartupWMClass for better app-id matching +* Avoid flicker when unblanking lock screen with media widget present +* Add status page to feedback quick settings that allows to enable + "Do not disturb" and to go to the feedback settings easily +* Fix showing of active network on Wi-Fi status page when the network + has multiple access points +* Don't let OSD block input on the whole screen when shown +* Disable mobile data quick settings when SIM is locked +* More GTK4 preps +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1222 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1183 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1095 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1180 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1218 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1224 +* Contributors: + * Arun Mani J + * fossdd + * Guido Günther +* UI translations: + * Álvaro Burns (pt_BR) + * Anders Jonsson (sv) + * Andi Chandler (en_GB) + * Antonio Marin (ro) + * Artur S0 (ru) + * Baxrom Raxmatov (uz) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Martin (sl) + * twlvnn kraftwerk (bg) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.46.0 +------------ +Released March 2025 +* lockscreen: Allow to set background image. Off by default, can be enabled + via mobile-settings (or `gsettings`). +* Close quick-settings status pages when closing the top panel +* Notifications: Use slide down animation in the message tray and allow to focus + default action +* media-player: Use `can-play` to device whether we should show the widget +* media-player: Download cover-art when possible +* layout-manager: Handle notches/cutouts in the indicator areas. This + helps devices with a e.g. camera in the left display corner (like + the Nothing Phone). +* Use more consistent button press feedback +* wifi-quick-setting: Indicate ongoing Wi-Fi scans in the status page +* Add `phosh.gsettings(5)` manpage. +* Set more wallpaper friendly defaults and improve legibility, reduce + flicker, fix thumbnail scaling and other fixes. +* Simplify UI files and other bits preps for a GTK4 migration +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1138 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1208 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1136 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1152 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1217 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1220 +* Contributors: + * Arun Mani J + * Bardia Moshiri + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * Sam Day + * Sebastian Krzyszkowiak + * Sicelo A. Mhlongo +* UI translations: + * Vasil Pupkin (be) + * Bruce Cowan (en_GB) + * Vincent Chatelain (fr) + * Andika Triwidada (id) + * Daniel Șerbănescu (ro) + * Jordi Mas i Hernandez (ca) + * Yaron Shahrabani (he) + +phosh 0.45.0 +------------ +Released February 2025 +* Detect captive portals and allow to open browser +* New quick setting plugin to switch display scales +* Create thumbnails for Screenshots +* Allow to uninstall apps from app-grid +* Close quick setting status pages when action was successful +* Add 'sound' capbility to notification server +* WWan enable/disable autoconnect with the connection so e.g. + mobile data doesn't turn on on resume +* Minimize and stabilize libphosh API +* prefs plugins: Remove the last remaining GtkDialog and use + AdwEntryRow +* Respect week number setting in calendar widget +* Avoid crash when fixing between certain combinations of fractional + scales (with e.g. wlr-randr) +* Allow libphosh users to hide the whole overview/bottom bar +* Fade in system modal dialogs +* Several background image related fixes +* Fix crash on unlock with media player widget +* Simplify most UI files +* Issues fixed: + * https://gitlab.gnome.org/guidog/meta-phosh/-/issues/16 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/989 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1139 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1159 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1166 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1169 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1171 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1173 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1176 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1177 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1181 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1184 +* Contributors: + * Adam Honse + * Arun Mani J + * Cédric Bellegarde + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * Sam Day +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Martin (sl) + * Rafael Fontenelle (pt_BR) + * Sabri Ünal (tr) + * twlvnn kraftwerk (bg) + * Yuri Chornoivan (uk) + +phosh 0.44.0 +------------ +Released December 2024 +* Set background in overview +* Allow to unfullscreen fullscreen apps from the overview +* Handle Cell broadcast messages. Shown as system modal dialog + (needs yet unreleased ModemManger / libmm-glib) +* Use AdwPreferencesDialog for prefs plugins giving more mobile + friendly dialogs +* Notification style fixes +* Animate hiding notification banners +* Internal cleanups and fixes +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1012 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/901 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/377 +* Contributors: + * Gotam Gorabh + * Guido Günther +* UI translations: + * Alexandre Franke (fr) + * Ekaterine Papava (ka) + * Hugo Carvalho (pt) + * Jordi Mas i Hernandez (ca) + * Pierre Michel Augustin (ht) + * Tim Sabsch (de) + * Yaron Shahrabani (he) + * twlvnn kraftwerk (bg) + +phosh 0.43.0 +------------ +Released November 2024 +* Improved quick settings + - Do away with long press to open status pages + - Show status pages below quick settings + - Custom quick settings can now have status pages too +* Support accent colors +* New quick setting: Simple Pomodoro timer (can be + configured via Mobile Settings) +* Notification system improvements and fixes, e.g. + - Fix some banners not showing + - Animate hiding applications in the tray and more fixes + - More consistent event feedback for notifications (so that + e.g. there's not only LED feedback when the device is + unused/locked) + - Try harder to find an application item +* Allow to disable lockscreen authentication +* Save screenshots to screenshots folder +* Lots of other fixes, focus and style improvements +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1090 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1118 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1124 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1125 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1127 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1130 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1132 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1133 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1134 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1143 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1144 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/221 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/351 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/728 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/928 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/964 + * https://source.puri.sm/Librem5/feedbackd/-/merge_requests/69 +* Contributors: + * Anna (cybertailor) Vyalkova + * Arun Mani J + * Eugenio Paolantonio (g7) + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * Kenny Levinsen + * Sam Day + * Teemu Ikonen +* UI translations: + * Anders Jonsson (sv) + * Andi Chandler (en_GB) + * Antonio Marin (ro) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Jordi Mas i Hernandez (ca) + * Juliano de Souza Camargo (pt_BR) + * Martin (sl) + * Nathan Follens (nl) + * Rafael Fontenelle (pt_BR) + * Vincent Chatelain (fr) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.42.0 +------------ +Released September 2024 +* lockscreen: Allow to adjust to smaller screen sizes +* lockscreen: Swap horizontal deck and vertical carousel to not accidentally + trigger swipe when entering PIN +* upcoming-events plugin: Allow to configure number of days +* torch: Hide brightness slider when there's only a single brightness level +* Improve notification appearance when there's multiple notifications from the + same app +* Use parent's icon for toplevels without a proper matching icon +* Handle WWan network types better +* Make Wi-Fi hotspot insensitive on lock screen to be consistent + with Wi-Fi network switching. +* Make compatibility with g-s-d 47 +* In preparation of Cell Broadcast support use mm-glib for + ModemManager interaction +* Ease setup and lib versioning for libphosh-rs users +* Fix crashes related to scale changes, Wi-Fi hotspot and other bug + fixes +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1050 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1027 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/901 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1104 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1108 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1115 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/899 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/970 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/858 +* Contributors: + * Arun Mani J + * Evangelos Ribeiro Tzaras + * Gotam Gorabh + * Guido Günther + * Jared Toomey + * Sam Day +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Balázs Úr (hu) + * Bruce Cowan (en_GB) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Juliano de Souza Camargo (pt_BR) + * Jürgen Benvenuti (de) + * Martin (sl) + * Sabri Ünal (tr) + * Vasil Pupkin (be) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.41.0 +------------ +Released August 2024 +* media-player: Add track position, length and progress bar +* Add (optional) Wi-Fi Hotspot Quick Setting +* Add Bluetooth Quick Setting status page allowing to + connect/disconnect known devices +* Allow to (optionally) put the phone in silent mode when pressing Vol- + on incoming calls +* Give Quick Setting status pages (and their individual rows) more + vertical space so selecting from a large list of Wi-Fi networks or + Bluetooth devices becomes less fiddly +* Fix keypad layouts in RTL languages +* Allow lock screen to have an extra page (mostly useful to libphosh + consumers) +* Switch all git submodules to meson subprojects (following what we + did in phoc a while back) +* Fix remaining introspection nits so we can ran gir-scanner and gi-docgen + with --fatal-warnings +* Lots of internal improvements e.g. allowing us to do new status pages + with less boilerplate +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1092 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/922 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/780 +* Contributors: + * Arun Mani J + * Cédric Bellegarde + * Evangelos Ribeiro Tzaras + * Guido Günther + * Sam Day + * Teemu Ikonen + * Xiao Pan +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Balázs Úr (hu) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Jürgen Benvenuti (de) + * Martin (sl) + * Sabri Ünal (tr) + * Scrambled 777 (hi) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.40.0 +------------ +Released June 2024 +* New quick setting plugins: + * Dark style toggle + * Mobile data toggle +* Other improvements: + * Allow to suspend when device is locked + * Shorten power menu long press delay + * Wire up screenshot keybinding + * Tweak multiple gsetting overrides (default idle-delay, unlock + sim, sound theme, …) + * Fix too large and too small icons in folder buttons + * launcher-box: Properly align count and progress to not look out + of place + * Move launched processes to transient scope + * Split settings enums into separate header so it can be shared with + mobile settings. + * Further speed up build by building a library for the stubs + * session: Drop support for `--builtin` session fallback + * Fix background loading related crash + * Add option to install shared lib for Rust binding generation + * Allow lockscreen and shell classes to be overridable from the + Rust bindings + * Add modem mock mocks test status and mobile data connections +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/691 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1069 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1069 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/990 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/931 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/963 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1071 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1073 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1072 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1036 +* Contributors: + * Arun Mani J + * Gotam Gorabh + * Guido Günther + * Sam Day + * Teemu Ikonen +* UI translations: + * Anders Jonsson (sv) + * Antonio Marin (ro) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Martin (sl) + * Scrambled 777 (hi) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + +phosh 0.39.0 +------------ +Released May 2024 +* Support folders to organize apps in the overview +* Optional night light quick setting +* Lots of introspection/documentation fixups in preparation for the Rust bindings +* Test suite improvements (stable clock, use shell state instead of + timeouts to track state changes, mpris metadata, …) +* Split generated headers and sources to speed up build and save CI resources +* Improve translatability of timestamp labels +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1042 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1056 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1058 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/785 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/868 +* Contributors: + * Arun Mani J + * Guido Günther + * Sam Day +* UI translations: + * Anders Jonsson (sv) + * Andi Chandler (en_GB) + * Andika Triwidada (id) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Jordi Mas i Hernandez (ca) + * Martin (sl) + * Quentin PAGÈS (oc) + * Sabri Ünal (tr) + * Scrambled 777 (hi) + * Yaron Shahrabani (he) + * Yuri Chornoivan (uk) + +phosh 0.38.0 +------------ +Released April 2024 +* Allow launcher entries to display count and progress +* Better handle devices with rounded corners +* Handle data: URIs in the media-player +* OSD improvements in case there's no level +* Fix some background scaling related issued introdueced in 0.37.0 +* Fix session startup with gnome-session 46 +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1035 +* Contributors: + * Daniel Rusek + * Guido Günther + * Jesús Higueras +* UI translations: + * Anders Jonsson (sv) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Jürgen Benvenuti (de) + * Martin (sl) + * Sabri Ünal (tr) + * Vittorio Monti (it) + * Yuri Chornoivan (uk) + +phosh 0.37.0 +------------ +Released March 2024 +* Allow to select Wi-Fi network via quick setting +* Allow plugins to implement quick settings +* Allow plugins to use selected phosh internals +* Load background images async +* Add caffeine quick setting +* More bug, keyboard navigation and style fixes +* Issues fixed: + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1025 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/447 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/910 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/743 + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/829 +* Contributors: + * Arun Mani J + * Guido Günther + * Rodney Lorrimar + * Sicelo A. Mhlongo +* UI translations: + * Anders Jonsson (sv) + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Ekaterine Papava (ka) + * Florentina Mușat (ro) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Jürgen Benvenuti (de) + * Pablo Barciela (es) + * Sabri Ünal (tr) + * Vittorio Monti (it) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + +phosh 0.36.0 +------------ +Released February 2024 +* Allow to tweak home bars's osk long press delay +* Drop home bar's animated arrow +* Improve app-id mappings +* Don't hard code Cantarell Font +* Pick up night light on newly plugged screens +* Unbreak service file with newer systemd +* More bug fixes and cleanups +* Issues fixed: + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1016 + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1019 +* Contributors: + * Anna “CyberTailor” + * Bardia Moshiri + * Guido Günther + * Kai Lüke + * Sertonix +* UI translations: + * Martin (sl) + * Pierre Michel Augustin (ht) + +phosh 0.35.0 +------------ +Released January 2024 +* Make home bar smaller. OSK opens via long press on pill. +* Indicate 5G when detected +* Fix suspend-inhibitor when hotspot is on +* Fix swipe back when lockscreen widget box is open +* Issues fixed: + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1010 + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1001 + - https:// gitlab.gnome.org/World/Phosh/phosh/-/issues/380 +* Contributors: + * Bardia Moshiri + * Guido Günther + * mathew-dennis +* UI translations: + * Artur S0 (ru) + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Yosef Or Boczko (he) + +phosh 0.34.0 +------------ +Released December 2023 +* End session dialog improvements +* Bring back night light +* Use ext-idle-notify-v1 Wayland protocol (requires phoc >= 0.33.0) +* Bump dependencies: Switch to gmobile 0.0.4 and libcall-ui 0.1.1 +* Small UI improvements +* CI improvements: Check coding style, improve ci-fairy checks +* Issues fixed: + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/994 + - https://gitlab.gnome.org/World/Phosh/phosh/-/issues/875 + - https://source.puri.sm/Librem5/OS-issues/-/issues/343 +* Contributors: + * Arun Mani J + * Guido Günther +* UI translations: + * Artur S0 (ru) + * Jordi Mas i Hernandez (ca) + * Juliano de Souza Camargo (pt_BR) + * Vittorio Monti (it) + +phosh 0.33.0 +------------ +Released November 2023 +* Add launcher lock screen plugin +* Allow to show password in all system modal prompts +* Issues fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/848 +* Contributors: + * Guido Günther + * pseudorandom-x +* UI translations: + * Anders Jonsson (sv) + * Emin Tufan Çetin (tr) + * Florentina Mușat (ro) + * Sabri Ünal (tr) + * Yuri Chornoivan (uk) + +phosh 0.32.0 +------------ +Released October 2023 +* Allow apps to suppress notification feedback +* Hide separator when there are no favorites +* Feedback robustness +* Doc updates +* Officially deprecate reading `rootston.ini` +* Issues fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/730 +* Contributors: + * Guido Günther + * Keegan Sabo +* UI translations: + * Tjipke van der Heide (fy) + * Vasil Pupkin (be) + +phosh 0.31.1 +------------ +Released September 2023 +* Fix version in meson file +* Contributors: + * Guido Günther + +phosh 0.31.0 +------------ +Released September 2023 +* Ship portal configuration for xdg-desktop-portal +* Handle tablet-mode-switch of convertibles +* Contributors: + * Guido Günther + * Simon McVittie +* UI translations: + * Artur S0 (ru) + * Asier Sarasua Garmendia (eu) + * Boyuan Yang (zh_CN) + * Efstathios Iosifidis (el) + * Fran Dieguez (gl) + * Jürgen Benvenuti (de) + * Martin (sl) + * Nathan Follens (nl) + * Sabri Ünal (tr) + +phosh 0.30.0 +------------ +Released August 2023 +* Save screenshots at full resolution +* Fix missing icon when e.g. connecting to a car's audio unit via BT +* Improve ongoing call notification. +* Update gvc submodule fixing a crash when restart pw-pulse +* Don't require gsd-xsettings in the session. No need to spawn + XWayland for that. +* Update gmobile to support more notches (Poco F1 and FF4) +* Issues fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/968 +* Contributors: + * anteater + * Guido Günther +* UI translations: + * Balázs Úr (hu) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Jürgen Benvenuti (de) + * Martin (sl) + * Piotr Drąg (pl) + * Yosef Or Boczko (he) + +phosh 0.29.0 +------------ +Released July 2023 +* Add audio device details and selection to settings +* Use animation with automatic HighContrast to indicate what is + happening +* Automatically avoid notches when device information is available +* Add lockscreen notification about ongoing calls +* Allow lockscreen notifications to take more vertical space +* Allow to suspend from system menu + ([when enabled](https://gitlab.gnome.org/World/Phosh/phosh/-/wikis/Configuration)) +* Fade out system modal dialogs +* Emergency info preferences plugin robustness +* Robustness fixes around notifications and feedback +* Make ASAN tests more useful again +* Issues fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/591 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/593 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/890 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/922 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/951 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/955 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/957 +* Contributors: + * Guido Günther + * Sam Hewitt + * undef +* UI translations: + * Anders Jonsson (sv) + * Danial Behzadi (fa) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Jiri Grönroos (fi) + * Jordi Mas i Hernandez (ca) + * Martin (sl) + * Piotr Drąg (pl) + * Prasanta Hembram (sat) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + +phosh 0.28.0 +------------ +Released May 2023 +* Allow notifications to wake up the screen. Can be configured by + category and urgency. +* Allow lockscreen to work on smaller displays +* Allow to reboot / log out from the settings power menu +* Styling improvements in several places +* Smoothen transition from splash to application +* Handle super key to open overview +* Handle auto repeat on grabbed keys (e.g. volume keys) +* Update to libcall-ui 0.1.0 +* Bump minimal supported GNOME versions to 42 and glib to 2.72 +* Issues fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/525 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/934 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/563 +* Contributors: + * Alistair Francis + * Eugenio Paolantonio (g7) + * Evangelos Ribeiro Tzaras + * Guido Günther + * Sam Hewitt + * Suraj Kumar Mahto + * Thomas Booker +* UI translations: + * Danial Behzadi (fa) + * Daniel Rusek (cs) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Jordi Mas i Hernandez (ca) + * Michael Oppliger (de) + * Pablo Correa Gómez (es) + * Piotr Drąg (pl) + * Vittorio Monti (it) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + +phosh 0.27.0 +------------ +Released May 2023 +* Initial emergency call support (needs to be enabled via GSetting) +* New menu on power button long press +* Allow to take fullscreen screenshots without external tools +* Honor applications feedback level for notifications +* Styling and layout improvements for system modal dialog +* Issues Fixed: + https://gitlab.gnome.org/GNOME/calls/-/issues/30 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/671 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/914 +* Contributors: + * CoderThomasB + * Guido Günther + * Sam Hewitt +* UI translations: + * Aleksandr Melman (ru) + * Anders Jonsson (sv) + * Danial Behzadi (fa) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Fran Dieguez (gl) + * Jiri Grönroos (fi) + * Martin (sl) + * Piotr Drąg (pl) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + * Мирослав Николић (sr) + +phosh 0.26.0 +------------ +Released April 2023 +* Inhibit suspend when WiFi hotspot is active +* Test suite improvements +* Power button handling improvements +* Avoid deprecations with GTK 4.10 in preferences plugins +* Make sure quick settings take enough vertical + space on the lock screen to avoid a needless scrollbar +* Issues Fixed: + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/903 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/907 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/909 + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/911 +* Contributors: + * Guido Günther + * Sam Hewitt + * Newbyte +* UI translations: + * Aleksandr Melman (ru) + * Anders Jonsson (sv) + * Asier Sarasua Garmendia (eu) + * Balázs Úr (hu) + * Danial Behzadi (fa) + * Daniel Șerbănescu (ro) + * Hugo Carvalho (pt) + * Jiri Grönroos (fi) + * Jürgen Benvenuti (de) + * Martin (sl) + * Piotr Drąg (pl) + * Sabri Ünal (tr) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + * Nathan Follens (nl) + +phosh 0.25.1 +------------ +Released March 2023 +* Avoid race that might end up with two overlaying lock screens + e.g. after suspend (#838) +* Limit slider width in settings (#898) +* Contributors: + * Guido Günther + * Newbyte + * Sam Hewitt + +phosh 0.25.0 +------------ +Released March 2023 +* New plugin to configure emergency contact preferences +* Quick Settings menu style refresh +* Separate locking from blanking +* Animate status icon transitions +* Improve settings menu usability in landscape +* Lots of internal cleanups +* Contributors: + * Cédric Bellegarde + * Chris Talbot + * Guido Günther + * Sam Hewitt +* UI translations: + * Anders Jonsson (sv) + * Danial Behzadi (fa) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Hemish (hi) + * Jürgen Benvenuti (de) + * Martin (sl) + * Yosef Or Boczko (he) + * Yuri Chornoivan (uk) + +phosh 0.24.0 +------------ +Released January 2023 +* Add manpages +* Various stability fixes +* Better detect logind session (to unbreak `loginctl (un)lock-session`) +* Testsuite improvements +* Update to gmobile 0.0.1 +* Honor stricter protocol implementation in wlroots 0.16.0 +* Contributors: + * Andrey Skvortsov + * Guido Günther +* UI translations: + * Aleksandr Melman (ru) + * Anders Jonsson (sv) + * Daniel Șerbănescu (ro) + * Ekaterine Papava (ka) + * Emin Tufan Çetin (tr) + * Goran Vidović (hr) + * Hemish (hi) + * Hugo Carvalho (pt) + * Jürgen Benvenuti (de) + * Martin (sl) + * Yuri Chornoivan (uk) + +phosh 0.23.0 +------------ +Released December 2022 +* New lockscreen plugin for personal/emergency information +* Allow plugins to have UI to set preferences +* Ease creating plugin by better examples, improved helpers + and less duplication. +* Switch docs to gi-docgen +* Contributors: + Chris Talbot + Evangelos Ribeiro Tzaras + Guido Günther +* UI translations: + Aleksandr Melman (ru) + Hugo Carvalho (pt) + Martin (sl) + Yosef Or Boczko (he) + Zurab Kargareteli (ka) + +phosh 0.22.0 +------------ +Released October 2022 +* Use 10% steps for battery info icon +* Honor suspend time in upcoming events refresh +* Allow notifications to have actions on the lock screen +* Activity close-button style refresh +* Contributors: + codewood + Guido Günther + Sam Hewitt +* UI translations: + Anders Jonsson (sv) + Balázs Úr (hu) + Daniel Șerbănescu (ro) + Hugo Carvalho (pt) + Nathan Follens (nl) + Vittorio Monti (it) + Vojtěch Vengrin (cs) + Yuri Chornoivan (uk) + Zurab Kargareteli (ka) + Danial Behzadi + Balázs Úr + +phosh 0.21.1 +------------ +Released September 2022 +* Bug fixes and cleanups (also to catch up with glib 2.74) +* Update to libcall-ui 0.0.5 +* Lots of translation updates +* Contributors: + Evangelos Ribeiro Tzaras + Fiona Klute + Guido Günther + Sam Hewitt +* UI translations: + Aleksandr Melman (ru) + Anders Jonsson (sv) + Danial Behzadi (fa) + Daniel Șerbănescu (ro) + Emin Tufan Çetin (tr) + Goran Vidović (hr) + Jiri Grönroos (fi) + Jürgen Benvenuti (de) + Martin (sl) + Piotr Drąg (pl) + Rūdolfs Mazurs (lv) + Vittorio Monti (it) + Yosef Or Boczko (he) + Yuri Chornoivan (uk) + Zurab Kargareteli (ka) + Марко Костић (sr) + +phosh 0.21.0 +------------ +Released September 2022 +* Add (experimental) upcoming-events lock screen + widget +* Allow to disable GTK animations again +* Improve screenshot support +* Report a version via DBus +* Update libcall-ui to 0.0.4 +* More bug fixes +* Contributors: + Evangelos Ribeiro Tzaras + Guido Günther + Sebastian Krzyszkowiak +* UI translations: + Balázs Úr (hu) + Emin Tufan Çetin (tr) + Goran Vidović (hr) + Jiri Grönroos (fi) + Yuri Chornoivan (uk) + +phosh 0.20.0 +------------ +Released August 2022 +* more bug fixes related to lockscreen, + drag surfaces and high contrast. +* Allow to switch to high contrast theme based on + ambient sensor +* Contributors: + Guido Günther + Sam Hewitt + Sebastian Krzyszkowiak + +phosh 0.20.0~beta3 +------------------ +Released July 2022 +* This is a beta release +* Many bug fixes related to the overview, lockscreen, osk handling, + screen rotation, tests and drag_surfaces. +* Support plugins on the lock screen +* Add a symbolic icon for phosh +* Contributors: + Alexander Mikhaylenko + Guido Günther + Sam Hewitt + Sebastian Krzyszkowiak +* UI translations: + Efstathios Iosifidis (el) + Vittorio Monti (it) + Zurab Kargareteli (ka) + Мирослав Николић (sr) + +phosh 0.20.0~beta2 +------------------- +Released June 2022 +* This is a beta release +* Fix top-bar's layer depending on state (locked, etc.) +* Fix multiple visual glitches related to overview and settings +* Improve OSK handling +* Switch to libcall-ui 0.0.3 +* Fix rotation manager not getting an output for some + display types +* Style improvements for notifications +* Contributors: + Evangelos Ribeiro Tzaras + Gerben Jan Dijkman + Guido Günther + Linus Walleij + Pablo Barciela + Sam Hewitt + Sebastian Krzyszkowiak +* UI translations: + Aleksandr Melman (ru) + Anders Jonsson (sv) + Danial Behzadi (fa) + Daniel Șerbănescu (ro) + Jiri Grönroos (fi) + Jürgen Benvenuti (de) + Nathan Follens (nl) + Piotr Drąg (pl) + Yosef Or Boczko (he) + Yuri Chornoivan (uk) + +phosh 0.20.0~beta1 +------------------- +Released May 2022 +* This is a beta release +* Home- and top-bar swipe gesture support +* Make settings and top-bar available on lock screen +* Move settings menu closer to designs +* Feedback indicator in top-bar +* Allow to view app details +* New DisplayConfig properties like + ApplyMonitorsConfigAllowed and NightLightSupported. +* Another large round of cleanups and fixes + including a fix for the HKS indicator +* Contributors: + Arnaud Ferraris + Guido Günther + InsanePrawn + Nathan Sherwood + Pablo Correa Gómez + Sam Hewitt + Sebastian Krzyszkowiak + Thomas +* UI translations: + Balázs Úr (hu) + Danial Behzadi (fa) + Daniel Șerbănescu (ro) + Emin Tufan Çetin (tr) + Hugo Carvalho (pt) + Jiri Grönroos (fi) + Luna Jernberg (sv) + Nathan Follens (nl) + Pablo Barciela (es) + Piotr Drąg (pl) + Quentin PAGÈS (oc) + Vittorio Monti (it) + Yosef Or Boczko (he) + Yuri Chornoivan (uk) + Zurab Kargareteli (ka) + +phosh 0.17.0 +------------ +Released: March 2022 +* Access Portal support +* Mobile data indicator +* Suspend after resume fixes +* Contributors: + Florian Loers + Guido Günther + Kai Lüke + Pablo Barciela + Sam Hewitt + ZenWalker +* UI translations: + Emin Tufan Çetin (tr) + free software (es) + Hugo Carvalho (pt) + Jiri Grönroos (fi) + Martin (sl) + Pablo Barciela (es) + Piotr Drąg (pl) + Prasanta Hembram (sat) + Мирослав Николић (sr) + +phosh 0.16.0 +------------ +Released: February 2022 +* Fading labels in the overview +* Keypad can shuffle buttons +* More style refresh +* Better ASAN coverage in tests +* Update to libcall-ui 0.0.2 +* Plenty of fixes and cleanups +* Contributors: + Adrien Plazas + Evangelos Ribeiro Tzaras + Guido Günther + Pablo Barciela + Sam Hewitt +* UI translations: + Danial Behzadi (fa) + Daniel Șerbănescu (ro) + Fran Dieguez (gl) + Marc Riera (ca) + Matheus Barbosa (pt_BR) + Yosef Or Boczko (he) + +phosh 0.15.0 +------------ +Released: January 2022 +* Swipeable notification frames +* VPN quicksettings, authentication and status icon +* "Run command" prompt improvement +* First parts of style refresh +* Make osk-stub more useful for debugging +* Hide some quick settings when hardware is not present +* Bring back gamma control protocol support +* Allow arbitrary passwords +* Rather large number of fixes +* Contributors: + Alexander Mikhaylenko + Evangelos Ribeiro Tzaras + Guido Günther + Pablo Barciela + PanzerSajt + Sam Hewitt + Sebastian Krzyszkowiak +* UI translations: + Anders Jonsson (sv) + Danial Behzadi (fa) + Daniel Șerbănescu (ro) + Fabio Tomat (fur) + Hugo Carvalho (pt) + Nathan Follens (nl) + Rafael Fontenelle (pt_BR) + Vittorio Monti (it) + Yuri Chornoivan (uk) + Matej Urbančič (sl) + +phosh 0.14.1 +------------ +Released: December 2021 +* Show avatars during phone calls on lockscreen +* Allow for DTMF during phone calls on lockscreen +* Improve docked mode when phone screen is off +* Improve thumbnails in overview +* Add initial "Run command" prompt (Alt-F2) +* Plenty of bug fixes and cleanups +* Contributors: Evangelos Ribeiro Tzaras, Sebastian Krzyszkowiak, Florian Loers, + Dorota Czaplejewicz, Hunman, Pablo Barciela, Guido Günther +* UI translations: + Anders Jonsson (sv) + Luna Jernberg (sv) + Yuri Chornoivan (uk) + +phosh 0.14.0 +------------ +Released: October 2021 +* Launch splash support (Guido Günther) +* Move "Show all apps" toggle to the bottom (Guido Günther) +* Media player widget enhancements (Oliver Smith, Guido Günther) +* Fade in shell on startup to avoid flicker (Guido Günther) +* App icons on activities are now centered (Florian Loers) +* Test suite generates screenshots (Guido Günther) +* Wifi hotspot mode indicated in the top bar (Mohammed Sadiq) +* Music player is paused when headphones get unplugged (Guido Günther) +* Better app_id handling (Sebastian Krzyszkowiak) +* Some activity rendering improvements (Guido Günther) +* Some leak fixes +* UI translations: + Danial Behzadi (fa) + Goran Vidović (hr) + Hugo Carvalho (pt) + Jiri Groenroos (fi) + Matej Urbančič (sl) + Nathan Follens (nl) + Vittorio Monti (it) + Zander Brown (en_GB) + +phosh 0.13.1 +------------ +Released: August 2021 +* Cycle through all feedback profiles in quick setting + (Pablo Correa Gómez) +* Add button to close all notifications (Guido Günther) +* Improve support for mounting encrypted media (Guido Günther) +* Don't launch app twice when keyboard activated from search bar + (Guido Günther) +* Improve fractional scaling support (Guido Günther) +* Better media player styling (Guido Günther) +* UI translations: + Daniel Șerbănescu (ro) + Kristjan SCHMIDT (eo) + Marc Riera (ca) + Michael Oppliger (de) + Andika Triwidada (id) + +phosh 0.13.0 +------------ +Released: August 2021 +* torch: Use logind for torch brightness. This obsoletes any upower changes. + (Arnaud Ferraris) +* Support high contrast mode (David Hamner, Guido Günther) +* ci: Use prebuilt docker images in CI to speedup builds and save resources + (Guido Günther) +* lockscreen: Handle incoming phone calls (Guido Günther) +* backgrounds: Handle fractional scaling (Guido Günther) +* notifications: Look at category for notification feedback +* lockscreen: Display notification summary and handle global + "show-in-lock-screen" toggle (Guido Günther) +* panel: Fix power menu close on tap (Mohammed Sadiq) +* quick settings: Cycle through all feedback settings instead of only + full / silent (Pablo Correa Gomez) +* Migrate to GNOME World (Andrea Veri, Guido Günther) +* UI translations: + Anders Jonsson (sv) + Efstathios Iosifidis (el) + Rafael Fontenelle (pt_BR) + Vittorio Monti (it) + Yuri Chornoivan (uk) + Мирослав Николић (sr) + +phosh 0.12.1 +------------ +Released: July 2021 +* Fix defaults for favorites +* Append 'Phosh' to XDG_CURRENT_DESKOP for the system unit too so overrides get + applied even when not using a display manager +* Bring search bar closer to designs again +* Simplify tests and test calls-manager. Fix leaks spotted by those. +* Don't claim accelerometer when rotation lock is on reducing iio-sensor-proxy + wakeups considerably +* i18n updates: uk, it, sv + +phosh 0.12.0 +------------ +Released: June 2021 +* Only enable proximity sensor on active calls, unblank screen on incoming + calls. This needs at least gnome-calls 0.3.4 and either one of + https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/3614 + https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2120 +* Implement most parts of org.Gtk.MountOperationHandler to handle + encrypted volume mounts in e.g. nautilus. +* Show adaptive apps in mobile mode and all apps in docked mode. This + can be toggled via the sm.puri.phosh.PhoshAppFilterModeFlags GSetting. + +phosh 0.11.0 +------------ +Released: May 2021 +* Wifi/WWAN/BT quick settings toggle on/off, long press opens Settings +* Initial support for gnome-session --systemd +* Torch brightness slider +* Allow to show battery percentage in top bar +* Fixes modal-dialog keyboard navigation +* Fixes crash with ja locale diff --git a/README.md b/README.md new file mode 100644 index 000000000..0150c81f3 --- /dev/null +++ b/README.md @@ -0,0 +1,130 @@ +# Phosh + +A pure Wayland shell for mobile devices like smart phones or small +tablets which + +* use touch input +* are running battery powered most of the time +* have limited screen space +* have a limited number of buttons +* might be docked to a keyboard, screen and other input devices + +Typical devices are the Librem 5 and PinePhone or devices formerly +running proprietary operating systems like the OnePlus 6/6T. + +The companion Wayland compositor is [phoc][]. + +## License + +phosh is licensed under the GPL-3.0-or-later licence. + +## Getting the source + +```sh +git clone https://gitlab.gnome.org/World/Phosh/phosh +cd phosh +``` + +The [main][] branch has the current development version. + +## Dependencies + +On a Debian based system run + +```sh +sudo apt -y install build-essential +sudo apt -y build-dep . +``` + +For an explicit list of dependencies check the `Build-Depends` entry in the +[debian/control][] file. + +## Building + +We use the meson (and thereby Ninja) build system for phosh. The quickest +way to get going is to do the following: + +```sh +meson setup _build +meson compile -C _build +``` + +## Testing + +To run the tests run + +```sh +xvfb-run meson test --no-suite screenshots -C _build +``` + +For details see the [.gitlab-ci.yml][] file. + +## Running + +### Running from the source tree + +When running from the source tree start the compositor *[phoc][]*. +Then start *phosh* using: + +```sh +_build/run +``` + +or (if you built *phoc* from source in *../phoc*) in one command: + +```sh +../phoc/_build/run -C ./data/phoc.ini -E _build/run +``` + +This will make sure the needed gsettings schema is found. Note that there's no +need to install any files outside the source tree. + +The result should look something like this: + +![phosh](screenshots/phosh-overview.png) + +### Running from the Debian packages + +If you're running a display manager like GDM or LightDM you can select the +`Phosh` session from the display managers menu. For development purposes +you can use the provided systemd unit: + +```sh +systemctl start phosh +``` + +This runs *phosh* as the user with user id 1000 (which needs to exist). If you +don't have that user and don't want to create one you can make systemd +run *phosh* as any user by using an override file: + +```sh +cat < /etc/systemd/system/phosh.service.d/override.conf +[Service] +User= +EOF +``` + +All of the above use the `/usr/bin/phosh-session` script to start compositor +and shell under the hood so if you just want to start phosh from the system +console once invoke that script directly. + +## Translations + +This is handled via GNOMEs infra, see + and +. + +## Getting in Touch + +* Issue tracker: +* Matrix: + +## Development Documentation + +Development documentation including public API docs and notes for application +developers is at . + +[main]: https://gitlab.gnome.org/World/Phosh/phosh/-/tree/main +[.gitlab-ci.yml]: https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/.gitlab-ci.yml +[debian/control]: https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/debian/control +[phoc]: https://gitlab.gnome.org/World/Phosh/phoc diff --git a/calendar-server/CalendarServer.service.in b/calendar-server/CalendarServer.service.in new file mode 100644 index 000000000..fd892ef17 --- /dev/null +++ b/calendar-server/CalendarServer.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=@app_id@.CalendarServer +Exec=@libexecdir@/@shell@-calendar-server diff --git a/calendar-server/calendar-debug.h b/calendar-server/calendar-debug.h new file mode 100644 index 000000000..0e10d6666 --- /dev/null +++ b/calendar-server/calendar-debug.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * SPDX-License-Identifier: GPL-3.0-or-later + + * Authors: + * Mark McLoughlin + */ + +#ifndef __CALENDAR_DEBUG_H__ +#define __CALENDAR_DEBUG_H__ + +#include + +G_BEGIN_DECLS + +#ifdef CALENDAR_ENABLE_DEBUG + +#include + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) fprintf (stderr, __VA_ARGS__); +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) fprintf (stderr, args); +#endif + +#else /* if !defined (CALENDAR_DEBUG) */ + +#ifdef G_HAVE_ISO_VARARGS +# define dprintf(...) +#elif defined(G_HAVE_GNUC_VARARGS) +# define dprintf(args...) +#endif + +#endif /* CALENDAR_ENABLE_DEBUG */ + +G_END_DECLS + +#endif /* __CALENDAR_DEBUG_H__ */ diff --git a/calendar-server/calendar-server.c b/calendar-server/calendar-server.c new file mode 100644 index 000000000..cdb89eef1 --- /dev/null +++ b/calendar-server/calendar-server.c @@ -0,0 +1,1102 @@ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: David Zeuthen + * + * Based on code from gnome-panel's clock-applet, file calendar-client.c, with Authors: + * + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + * + */ + +#include "phosh-config.h" + +#include +#include +#include + +#include + +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include +G_GNUC_END_IGNORE_DEPRECATIONS + +#include "calendar-sources.h" + +#define BUS_NAME PHOSH_APP_ID ".CalendarServer" + +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; +static GDBusNodeInfo *introspection_data = NULL; + +struct _App; +typedef struct _App App; + +static gboolean opt_replace = FALSE; +static GOptionEntry opt_entries[] = { + {"replace", 0, 0, G_OPTION_ARG_NONE, &opt_replace, "Replace existing daemon", NULL}, + {NULL } +}; +static App *_global_app = NULL; + +/* ---------------------------------------------------------------------------------------------------- */ + +/* While the UID is usually enough to identify an event, + * only the triple of (source,UID,RID) is fully unambiguous; + * neither may contain '\n', so we can safely use it to + * create a unique ID from the triple + */ +static gchar * +create_event_id (const gchar *source_uid, + const gchar *comp_uid, + const gchar *comp_rid) +{ + return g_strconcat ( + source_uid ? source_uid : "", + "\n", + comp_uid ? comp_uid : "", + "\n", + comp_rid ? comp_rid : "", + NULL); +} + +typedef struct +{ + ECalClient *client; + GSList **pappointments; /* CalendarAppointment * */ +} CollectAppointmentsData; + +typedef struct +{ + gchar *id; + gchar *summary; + time_t start_time; + time_t end_time; + char *color; +} CalendarAppointment; + +static gboolean +get_time_from_property (ECalClient *cal, + ICalComponent *icomp, + ICalPropertyKind prop_kind, + ICalTime * (* get_prop_func) (ICalProperty *prop), + ICalTimezone *default_zone, + ICalTime **out_itt, + ICalTimezone **out_timezone) +{ + ICalProperty *prop; + ICalTime *itt; + ICalTimezone *tz = NULL; + + prop = i_cal_component_get_first_property (icomp, prop_kind); + if (!prop) + return FALSE; + + itt = get_prop_func (prop); + + if (i_cal_time_is_utc (itt)) + tz = i_cal_timezone_get_utc_timezone (); + else + { + ICalParameter *param; + + param = i_cal_property_get_first_parameter (prop, I_CAL_TZID_PARAMETER); + if (param && !e_cal_client_get_timezone_sync (cal, i_cal_parameter_get_tzid (param), &tz, NULL, NULL)) + print_debug ("Failed to get timezone '%s'\n", i_cal_parameter_get_tzid (param)); + + g_clear_object (¶m); + } + + if (tz == NULL) + tz = default_zone; + + i_cal_time_set_timezone (itt, tz); + + g_clear_object (&prop); + + *out_itt = itt; + *out_timezone = tz; + + return TRUE; +} + +static inline time_t +get_ical_start_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *tz; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &tz)) + { + return 0; + } + + retval = i_cal_time_as_timet_with_zone (itt, tz); + + g_clear_object (&itt); + + return retval; +} + +static inline time_t +get_ical_end_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *tz; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, + default_zone, + &itt, + &tz)) + { + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &tz)) + { + return 0; + } + + if (i_cal_time_is_date (itt)) + i_cal_time_adjust (itt, 1, 0, 0, 0); + } + + retval = i_cal_time_as_timet_with_zone (itt, tz); + + g_clear_object (&itt); + + return retval; +} + +static CalendarAppointment * +calendar_appointment_new (ECalClient *cal, + ECalComponent *comp) +{ + CalendarAppointment *appt; + ICalTimezone *default_zone; + ICalComponent *ical; + ECalComponentId *id; + ESource *source; + + default_zone = e_cal_client_get_default_timezone (cal); + ical = e_cal_component_get_icalcomponent (comp); + id = e_cal_component_get_id (comp); + + appt = g_new0 (CalendarAppointment, 1); + + appt->id = create_event_id (e_source_get_uid (e_client_get_source (E_CLIENT (cal))), + id ? e_cal_component_id_get_uid (id) : NULL, + id ? e_cal_component_id_get_rid (id) : NULL); + appt->summary = g_strdup (i_cal_component_get_summary (ical)); + appt->start_time = get_ical_start_time (cal, ical, default_zone); + appt->end_time = get_ical_end_time (cal, ical, default_zone); + + source = e_client_get_source (E_CLIENT (cal)); + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + { + ESourceSelectable *ext = e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR); + const char *color; + + color = e_source_selectable_get_color (ext); + appt->color = g_strdup (color); + } + + e_cal_component_id_free (id); + + return appt; +} + +static void +calendar_appointment_free (gpointer ptr) +{ + CalendarAppointment *appt = ptr; + + if (appt) + { + g_free (appt->id); + g_free (appt->summary); + g_free (appt->color); + g_free (appt); + } +} + +static time_t +timet_from_ical_time (ICalTime *time, + ICalTimezone *default_zone) +{ + ICalTimezone *tz = NULL; + + tz = i_cal_time_get_timezone (time); + if (tz == NULL) + tz = default_zone; + return i_cal_time_as_timet_with_zone (time, tz); +} + +static gboolean +generate_instances_cb (ICalComponent *icomp, + ICalTime *instance_start, + ICalTime *instance_end, + gpointer user_data, + GCancellable *cancellable, + GError **error) +{ + CollectAppointmentsData *data = user_data; + CalendarAppointment *appointment; + ECalComponent *comp; + ICalTimezone *default_zone; + + default_zone = e_cal_client_get_default_timezone (data->client); + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); + + appointment = calendar_appointment_new (data->client, comp); + appointment->start_time = timet_from_ical_time (instance_start, default_zone); + appointment->end_time = timet_from_ical_time (instance_end, default_zone); + + *(data->pappointments) = g_slist_prepend (*(data->pappointments), appointment); + + g_clear_object (&comp); + + return TRUE; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +struct _App +{ + GDBusConnection *connection; + + time_t since; + time_t until; + + ICalTimezone *zone; + + CalendarSources *sources; + gulong client_appeared_signal_id; + gulong client_disappeared_signal_id; + + gchar *timezone_location; + + GSList *notify_appointments; /* CalendarAppointment *, for EventsAdded */ + GSList *notify_ids; /* gchar *, for EventsRemoved */ + + GSList *live_views; +}; + +static void +app_update_timezone (App *app) +{ + g_autofree char *location = NULL; + + location = e_cal_system_timezone_get_location (); + if (g_strcmp0 (location, app->timezone_location) != 0) + { + if (location == NULL) + app->zone = i_cal_timezone_get_utc_timezone (); + else + app->zone = i_cal_timezone_get_builtin_timezone (location); + g_free (app->timezone_location); + app->timezone_location = g_steal_pointer (&location); + print_debug ("Using timezone %s", app->timezone_location); + } +} + +static void +app_notify_events_added (App *app) +{ + GVariantBuilder builder; + GSList *events, *link; + + events = g_slist_reverse (app->notify_appointments); + app->notify_appointments = NULL; + + print_debug ("Emitting EventsAddedOrUpdated with %d events", g_slist_length (events)); + + if (!events) + return; + + /* The a{sv} is used as an escape hatch in case we want to provide more + * information in the future without breaking ABI + */ + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssxxa{sv})")); + for (link = events; link; link = g_slist_next (link)) + { + CalendarAppointment *appt = link->data; + time_t start_time = appt->start_time; + time_t end_time = appt->end_time; + GVariantBuilder extras_builder; + + if ((start_time >= app->since && + start_time < app->until) || + (start_time <= app->since && + (end_time - 1) > app->since)) + { + g_variant_builder_init (&extras_builder, G_VARIANT_TYPE ("a{sv}")); + if (appt->color) + { + g_variant_builder_add (&extras_builder, + "{sv}", + "color", + g_variant_new_string (appt->color)); + } + g_variant_builder_add (&builder, + "(ssxxa{sv})", + appt->id, + appt->summary != NULL ? appt->summary : "", + (gint64) start_time, + (gint64) end_time, + &extras_builder); + } + } + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + PHOSH_APP_ID ".CalendarServer", + "EventsAddedOrUpdated", + g_variant_new ("(a(ssxxa{sv}))", &builder), + NULL); + + g_variant_builder_clear (&builder); + + g_slist_free_full (events, calendar_appointment_free); +} + +static void +app_notify_events_removed (App *app) +{ + GVariantBuilder builder; + GSList *ids, *link; + + ids = app->notify_ids; + app->notify_ids = NULL; + + print_debug ("Emitting EventsRemoved with %d ids", g_slist_length (ids)); + + if (!ids) + return; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("as")); + for (link = ids; link; link = g_slist_next (link)) + { + const gchar *id = link->data; + + g_variant_builder_add (&builder, "s", id); + } + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + PHOSH_APP_ID ".CalendarServer", + "EventsRemoved", + g_variant_new ("(as)", &builder), + NULL); + g_variant_builder_clear (&builder); + + g_slist_free_full (ids, g_free); + + return; +} + +static void +app_process_added_modified_objects (App *app, + ECalClientView *view, + GSList *objects) /* ICalComponent * */ +{ + ECalClient *cal_client; + GSList *link; + gboolean expand_recurrences; + + cal_client = e_cal_client_view_ref_client (view); + expand_recurrences = e_cal_client_get_source_type (cal_client) == E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + + for (link = objects; link; link = g_slist_next (link)) + { + ECalComponent *comp; + ICalComponent *icomp = link->data; + + if (!icomp || !i_cal_component_get_uid (icomp)) + continue; + + if (expand_recurrences && + !e_cal_util_component_is_instance (icomp) && + e_cal_util_component_has_recurrences (icomp)) + { + CollectAppointmentsData data; + + data.client = cal_client; + data.pappointments = &app->notify_appointments; + + e_cal_client_generate_instances_for_object_sync (cal_client, icomp, app->since, app->until, NULL, + generate_instances_cb, &data); + } + else + { + comp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (icomp)); + if (!comp) + continue; + + app->notify_appointments = g_slist_prepend (app->notify_appointments, + calendar_appointment_new (cal_client, comp)); + g_object_unref (comp); + } + } + + g_clear_object (&cal_client); + + if (app->notify_appointments) + app_notify_events_added (app); +} + +static void +on_objects_added (ECalClientView *view, + GSList *objects, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); + + app_process_added_modified_objects (app, view, objects); +} + +static void +on_objects_modified (ECalClientView *view, + GSList *objects, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + + client = e_cal_client_view_ref_client (view); + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (objects), e_source_get_uid (e_client_get_source (E_CLIENT (client)))); + g_clear_object (&client); + + app_process_added_modified_objects (app, view, objects); +} + +static void +on_objects_removed (ECalClientView *view, + GSList *uids, + gpointer user_data) +{ + App *app = user_data; + ECalClient *client; + GSList *link; + const gchar *source_uid; + + client = e_cal_client_view_ref_client (view); + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); + + print_debug ("%s (%d) for calendar '%s'", G_STRFUNC, g_slist_length (uids), source_uid); + + for (link = uids; link; link = g_slist_next (link)) + { + ECalComponentId *id = link->data; + + if (!id) + continue; + + app->notify_ids = g_slist_prepend (app->notify_ids, + create_event_id (source_uid, + e_cal_component_id_get_uid (id), + e_cal_component_id_get_rid (id))); + } + + g_clear_object (&client); + + if (app->notify_ids) + app_notify_events_removed (app); +} + +static gboolean +app_has_calendars (App *app) +{ + return app->live_views != NULL; +} + +static ECalClientView * +app_start_view (App *app, + ECalClient *cal_client) +{ + g_autofree char *since_iso8601 = NULL; + g_autofree char *until_iso8601 = NULL; + g_autofree char *query = NULL; + const gchar *tz_location; + ECalClientView *view = NULL; + g_autoptr (GError) error = NULL; + + if (app->since <= 0 || app->since >= app->until) + return NULL; + + if (!app->since || !app->until) + { + print_debug ("Skipping load of events, no time interval set yet"); + return NULL; + } + + /* timezone could have changed */ + app_update_timezone (app); + + since_iso8601 = isodate_from_time_t (app->since); + until_iso8601 = isodate_from_time_t (app->until); + tz_location = i_cal_timezone_get_location (app->zone); + + print_debug ("Loading events since %s until %s for calendar '%s'", + since_iso8601, + until_iso8601, + e_source_get_uid (e_client_get_source (E_CLIENT (cal_client)))); + + query = g_strdup_printf ("occur-in-time-range? (make-time \"%s\") " + "(make-time \"%s\") \"%s\"", + since_iso8601, + until_iso8601, + tz_location); + + if (app->zone) + e_cal_client_set_default_timezone (cal_client, app->zone); + + if (!e_cal_client_get_view_sync (cal_client, query, &view, NULL /* cancellable */, &error)) + { + g_warning ("Error setting up live-query '%s' on calendar: %s\n", query, error ? error->message : "Unknown error"); + view = NULL; + } + else + { + g_signal_connect (view, + "objects-added", + G_CALLBACK (on_objects_added), + app); + g_signal_connect (view, + "objects-modified", + G_CALLBACK (on_objects_modified), + app); + g_signal_connect (view, + "objects-removed", + G_CALLBACK (on_objects_removed), + app); + e_cal_client_view_start (view, NULL); + } + return view; +} + +static void +app_stop_view (App *app, + ECalClientView *view) +{ + e_cal_client_view_stop (view, NULL); + + g_signal_handlers_disconnect_by_func (view, on_objects_added, app); + g_signal_handlers_disconnect_by_func (view, on_objects_modified, app); + g_signal_handlers_disconnect_by_func (view, on_objects_removed, app); +} + +static void +app_notify_has_calendars (App *app) +{ + GVariantBuilder dict_builder; + + g_variant_builder_init (&dict_builder, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&dict_builder, "{sv}", "HasCalendars", + g_variant_new_boolean (app_has_calendars (app))); + + g_dbus_connection_emit_signal (app->connection, + NULL, + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + PHOSH_APP_ID ".CalendarServer", + &dict_builder, + NULL), + NULL); + g_variant_builder_clear (&dict_builder); +} + +static void +app_update_views (App *app) +{ + GSList *link, *clients; + gboolean had_views, has_views; + + had_views = app->live_views != NULL; + + for (link = app->live_views; link; link = g_slist_next (link)) + { + app_stop_view (app, link->data); + } + + g_slist_free_full (app->live_views, g_object_unref); + app->live_views = NULL; + + clients = calendar_sources_ref_clients (app->sources); + + for (link = clients; link; link = g_slist_next (link)) + { + ECalClient *cal_client = link->data; + ECalClientView *view; + + if (!cal_client) + continue; + + view = app_start_view (app, cal_client); + if (view) + app->live_views = g_slist_prepend (app->live_views, view); + } + + has_views = app->live_views != NULL; + + if (has_views != had_views) + app_notify_has_calendars (app); + + g_slist_free_full (clients, g_object_unref); +} + +static void +on_client_appeared_cb (CalendarSources *sources, + ECalClient *client, + gpointer user_data) +{ + App *app = user_data; + ECalClientView *view; + GSList *link; + const gchar *source_uid; + + source_uid = e_source_get_uid (e_client_get_source (E_CLIENT (client))); + + print_debug ("Client appeared '%s'", source_uid); + + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *v = link->data; + ECalClient *cal_client; + ESource *source; + + cal_client = e_cal_client_view_ref_client (v); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + return; + } + + g_clear_object (&cal_client); + } + + view = app_start_view (app, client); + + if (view) + { + app->live_views = g_slist_prepend (app->live_views, view); + + /* It's the first view, notify that it has calendars now */ + if (!g_slist_next (app->live_views)) + app_notify_has_calendars (app); + } +} + +static void +on_client_disappeared_cb (CalendarSources *sources, + const gchar *source_uid, + gpointer user_data) +{ + App *app = user_data; + GSList *link; + + print_debug ("Client disappeared '%s'", source_uid); + + for (link = app->live_views; link; link = g_slist_next (link)) + { + ECalClientView *view = link->data; + ECalClient *cal_client; + ESource *source; + + cal_client = e_cal_client_view_ref_client (view); + source = e_client_get_source (E_CLIENT (cal_client)); + + if (g_strcmp0 (source_uid, e_source_get_uid (source)) == 0) + { + g_clear_object (&cal_client); + app_stop_view (app, view); + app->live_views = g_slist_remove (app->live_views, view); + g_object_unref (view); + + print_debug ("Emitting ClientDisappeared for '%s'", source_uid); + + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + PHOSH_APP_ID ".CalendarServer", + "ClientDisappeared", + g_variant_new ("(s)", source_uid), + NULL); + + /* It was the last view, notify that it doesn't have calendars now */ + if (!app->live_views) + app_notify_has_calendars (app); + + break; + } + + g_clear_object (&cal_client); + } +} + +static App * +app_new (GDBusConnection *connection) +{ + App *app; + + app = g_new0 (App, 1); + app->connection = g_object_ref (connection); + app->sources = calendar_sources_get (); + app->client_appeared_signal_id = g_signal_connect (app->sources, + "client-appeared", + G_CALLBACK (on_client_appeared_cb), + app); + app->client_disappeared_signal_id = g_signal_connect (app->sources, + "client-disappeared", + G_CALLBACK (on_client_disappeared_cb), + app); + + app_update_timezone (app); + + return app; +} + +static void +app_free (App *app) +{ + GSList *ll; + + for (ll = app->live_views; ll != NULL; ll = g_slist_next (ll)) + { + ECalClientView *view = E_CAL_CLIENT_VIEW (ll->data); + + app_stop_view (app, view); + } + + g_signal_handler_disconnect (app->sources, + app->client_appeared_signal_id); + g_signal_handler_disconnect (app->sources, + app->client_disappeared_signal_id); + + g_free (app->timezone_location); + + g_slist_free_full (app->live_views, g_object_unref); + g_slist_free_full (app->notify_appointments, calendar_appointment_free); + g_slist_free_full (app->notify_ids, g_free); + + g_object_unref (app->connection); + g_object_unref (app->sources); + + g_free (app); +} + +/* ---------------------------------------------------------------------------------------------------- */ + +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) +{ + App *app = user_data; + + if (g_strcmp0 (method_name, "SetTimeRange") == 0) + { + gint64 since; + gint64 until; + gboolean force_reload = FALSE; + gboolean window_changed = FALSE; + + g_variant_get (parameters, + "(xxb)", + &since, + &until, + &force_reload); + + if (until < since) + { + g_dbus_method_invocation_return_dbus_error (invocation, + PHOSH_APP_ID ".CalendarServer.Error.Failed", + "until cannot be before since"); + goto out; + } + + print_debug ("Handling SetTimeRange (since=%" G_GINT64_FORMAT ", until=%" G_GINT64_FORMAT ", force_reload=%s)", + since, + until, + force_reload ? "true" : "false"); + + if (app->until != until || app->since != since) + { + GVariantBuilder *builder; + GVariantBuilder *invalidated_builder; + + app->until = until; + app->since = since; + window_changed = TRUE; + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + invalidated_builder = g_variant_builder_new (G_VARIANT_TYPE ("as")); + g_variant_builder_add (builder, "{sv}", + "Until", g_variant_new_int64 (app->until)); + g_variant_builder_add (builder, "{sv}", + "Since", g_variant_new_int64 (app->since)); + g_dbus_connection_emit_signal (app->connection, + NULL, /* destination_bus_name */ + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + PHOSH_APP_ID ".CalendarServer", + builder, + invalidated_builder), + NULL); /* GError** */ + + g_variant_builder_unref (builder); + g_variant_builder_unref (invalidated_builder); + } + + g_dbus_method_invocation_return_value (invocation, NULL); + + if (window_changed || force_reload) + app_update_views (app); + } + else + { + g_assert_not_reached (); + } + + out: + ; +} + +static GVariant * +handle_get_property (GDBusConnection *connection, + const gchar *sender, + const gchar *object_path, + const gchar *interface_name, + const gchar *property_name, + GError **error, + gpointer user_data) +{ + App *app = user_data; + GVariant *ret; + + ret = NULL; + if (g_strcmp0 (property_name, "Since") == 0) + { + ret = g_variant_new_int64 (app->since); + } + else if (g_strcmp0 (property_name, "Until") == 0) + { + ret = g_variant_new_int64 (app->until); + } + else if (g_strcmp0 (property_name, "HasCalendars") == 0) + { + ret = g_variant_new_boolean (app_has_calendars (app)); + } + else + { + g_assert_not_reached (); + } + return ret; +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + handle_get_property, + NULL /* handle_set_property */ +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GMainLoop *main_loop = user_data; + guint registration_id; + g_autoptr (GError) error = NULL; + + _global_app = app_new (connection); + + registration_id = g_dbus_connection_register_object (connection, + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + introspection_data->interfaces[0], + &interface_vtable, + _global_app, + NULL, /* user_data_free_func */ + &error); + if (registration_id == 0) + { + g_printerr ("Error exporting object: %s (%s %d)\n", + error->message, + g_quark_to_string (error->domain), + error->code); + g_main_loop_quit (main_loop); + return; + } + + print_debug ("Connected to the session bus"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GMainLoop *main_loop = user_data; + + g_print (CALENDAR_SERVER_NAME "[%d]: Lost (or failed to acquire) the name " BUS_NAME " - exiting\n", + (gint) getpid ()); + g_main_loop_quit (main_loop); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Acquired the name " BUS_NAME); +} + +static gboolean +stdin_channel_io_func (GIOChannel *source, + GIOCondition condition, + gpointer data) +{ + GMainLoop *main_loop = data; + + if (condition & G_IO_HUP) + { + g_debug (CALENDAR_SERVER_NAME "[%d]: Got HUP on stdin - exiting\n", + (gint) getpid ()); + g_main_loop_quit (main_loop); + } + else + { + g_warning ("Unhandled condition %d on GIOChannel for stdin", condition); + } + return FALSE; /* remove source */ +} + +int +main (int argc, + char **argv) +{ + g_autoptr (GError) error = NULL; + GOptionContext *opt_context; + GMainLoop *main_loop; + gint ret; + guint name_owner_id; + GIOChannel *stdin_channel; + + ret = 1; + opt_context = NULL; + name_owner_id = 0; + stdin_channel = NULL; + + introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (introspection_data != NULL); + + opt_context = g_option_context_new (GETTEXT_PACKAGE "calendar server"); + g_option_context_add_main_entries (opt_context, opt_entries, NULL); + if (!g_option_context_parse (opt_context, &argc, &argv, &error)) + { + g_printerr ("Error parsing options: %s\n", error->message); + goto out; + } + + main_loop = g_main_loop_new (NULL, FALSE); + + stdin_channel = g_io_channel_unix_new (STDIN_FILENO); + g_io_add_watch_full (stdin_channel, + G_PRIORITY_DEFAULT, + G_IO_HUP, + stdin_channel_io_func, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + (opt_replace ? G_BUS_NAME_OWNER_FLAGS_REPLACE : 0), + on_bus_acquired, + on_name_acquired, + on_name_lost, + g_main_loop_ref (main_loop), + (GDestroyNotify) g_main_loop_unref); + + g_main_loop_run (main_loop); + + g_main_loop_unref (main_loop); + + ret = 0; + + out: + if (stdin_channel != NULL) + g_io_channel_unref (stdin_channel); + if (_global_app != NULL) + app_free (_global_app); + if (name_owner_id != 0) + g_bus_unown_name (name_owner_id); + if (opt_context != NULL) + g_option_context_free (opt_context); + + return ret; +} diff --git a/calendar-server/calendar-sources.c b/calendar-server/calendar-sources.c new file mode 100644 index 000000000..d9241ee25 --- /dev/null +++ b/calendar-server/calendar-sources.c @@ -0,0 +1,495 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#include + +#include "calendar-sources.h" + +#include +#include +#define HANDLE_LIBICAL_MEMORY +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include +G_GNUC_END_IGNORE_DEPRECATIONS + +#undef CALENDAR_ENABLE_DEBUG +#include "calendar-debug.h" + +typedef struct _ClientData ClientData; +typedef struct _CalendarSourceData CalendarSourceData; + +struct _ClientData +{ + ECalClient *client; + gulong backend_died_id; +}; + +typedef struct _CalendarSourcesPrivate CalendarSourcesPrivate; + +struct _CalendarSources +{ + GObject parent; + + ESourceRegistryWatcher *registry_watcher; + gulong filter_id; + gulong appeared_id; + gulong disappeared_id; + + GMutex clients_lock; + GHashTable *clients; /* ESource -> ClientData */ +}; + +G_DEFINE_TYPE (CalendarSources, calendar_sources, G_TYPE_OBJECT) + +enum +{ + CLIENT_APPEARED, + CLIENT_DISAPPEARED, + LAST_SIGNAL +}; +static guint signals [LAST_SIGNAL] = { 0, }; + +static void +calendar_sources_client_connected_cb (GObject *source_object, + GAsyncResult *result, + gpointer user_data) +{ + CalendarSources *sources = CALENDAR_SOURCES (source_object); + ESource *source = user_data; + EClient *client; + g_autoptr (GError) error = NULL; + + /* The calendar_sources_connect_client_sync() already stored the 'client' + * into the sources->clients */ + client = calendar_sources_connect_client_finish (sources, result, &error); + if (error) + { + g_warning ("Could not load source '%s': %s", + e_source_get_uid (source), + error->message); + } + else + { + g_signal_emit (sources, signals[CLIENT_APPEARED], 0, client, NULL); + } + + g_clear_object (&client); + g_clear_object (&source); +} + +static gboolean +registry_watcher_filter_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + return e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR) && + e_source_selectable_get_selected (e_source_get_extension (source, E_SOURCE_EXTENSION_CALENDAR)); +} + +static void +registry_watcher_source_appeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + ECalClientSourceType source_type; + + if (e_source_has_extension (source, E_SOURCE_EXTENSION_CALENDAR)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_MEMO_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_MEMOS; + else if (e_source_has_extension (source, E_SOURCE_EXTENSION_TASK_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + else + g_return_if_reached (); + + calendar_sources_connect_client (sources, source, source_type, 30, NULL, calendar_sources_client_connected_cb, g_object_ref (source)); +} + +static void +registry_watcher_source_disappeared_cb (ESourceRegistryWatcher *watcher, + ESource *source, + CalendarSources *sources) +{ + gboolean emit; + + g_mutex_lock (&sources->clients_lock); + + emit = g_hash_table_remove (sources->clients, source); + + g_mutex_unlock (&sources->clients_lock); + + if (emit) + g_signal_emit (sources, signals[CLIENT_DISAPPEARED], 0, e_source_get_uid (source), NULL); +} + +static void +client_data_free (ClientData *data) +{ + g_signal_handler_disconnect (data->client, data->backend_died_id); + g_object_unref (data->client); + g_free (data); +} + +static void +calendar_sources_constructed (GObject *object) +{ + CalendarSources *sources = CALENDAR_SOURCES (object); + ESourceRegistry *registry = NULL; + GError *error = NULL; + + G_OBJECT_CLASS (calendar_sources_parent_class)->constructed (object); + + registry = e_source_registry_new_sync (NULL, &error); + if (error != NULL) + { + /* Any error is fatal, but we don't want to crash the calendar-server + because of e-d-s problems. So just exit here. + */ + g_warning ("Failed to start evolution-source-registry: %s", error->message); + exit (EXIT_FAILURE); + } + + g_return_if_fail (registry != NULL); + + sources->registry_watcher = e_source_registry_watcher_new (registry, NULL); + + g_clear_object (®istry); + + sources->clients = g_hash_table_new_full ((GHashFunc) e_source_hash, + (GEqualFunc) e_source_equal, + (GDestroyNotify) g_object_unref, + (GDestroyNotify) client_data_free); + sources->filter_id = g_signal_connect (sources->registry_watcher, + "filter", + G_CALLBACK (registry_watcher_filter_cb), + sources); + sources->appeared_id = g_signal_connect (sources->registry_watcher, + "appeared", + G_CALLBACK (registry_watcher_source_appeared_cb), + sources); + sources->disappeared_id = g_signal_connect (sources->registry_watcher, + "disappeared", + G_CALLBACK (registry_watcher_source_disappeared_cb), + sources); + + e_source_registry_watcher_reclaim (sources->registry_watcher); +} + +static void +calendar_sources_finalize (GObject *object) +{ + CalendarSources *sources = CALENDAR_SOURCES (object); + + g_clear_pointer (&sources->clients, g_hash_table_destroy); + + if (sources->registry_watcher) + { + g_signal_handler_disconnect (sources->registry_watcher, + sources->filter_id); + g_signal_handler_disconnect (sources->registry_watcher, + sources->appeared_id); + g_signal_handler_disconnect (sources->registry_watcher, + sources->disappeared_id); + g_clear_object (&sources->registry_watcher); + } + + g_mutex_clear (&sources->clients_lock); + + G_OBJECT_CLASS (calendar_sources_parent_class)->finalize (object); +} + +static void +calendar_sources_class_init (CalendarSourcesClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *) klass; + + gobject_class->constructed = calendar_sources_constructed; + gobject_class->finalize = calendar_sources_finalize; + + signals [CLIENT_APPEARED] = + g_signal_new ("client-appeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + E_TYPE_CAL_CLIENT); + + signals [CLIENT_DISAPPEARED] = + g_signal_new ("client-disappeared", + G_TYPE_FROM_CLASS (gobject_class), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); /* ESource::uid of the disappeared client */ +} + +static void +calendar_sources_init (CalendarSources *sources) +{ + g_mutex_init (&sources->clients_lock); +} + +CalendarSources * +calendar_sources_get (void) +{ + static CalendarSources *calendar_sources_singleton = NULL; + gpointer singleton_location = &calendar_sources_singleton; + + if (calendar_sources_singleton) + return g_object_ref (calendar_sources_singleton); + + calendar_sources_singleton = g_object_new (CALENDAR_TYPE_SOURCES, NULL); + g_object_add_weak_pointer (G_OBJECT (calendar_sources_singleton), + singleton_location); + + return calendar_sources_singleton; +} + +ESourceRegistry * +calendar_sources_get_registry (CalendarSources *sources) +{ + return e_source_registry_watcher_get_registry (sources->registry_watcher); +} + +static void +gather_event_clients_cb (gpointer key, + gpointer value, + gpointer user_data) +{ + GSList **plist = user_data; + ClientData *cd = value; + + if (cd) + *plist = g_slist_prepend (*plist, g_object_ref (cd->client)); +} + +GSList * +calendar_sources_ref_clients (CalendarSources *sources) +{ + GSList *list = NULL; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), NULL); + + g_mutex_lock (&sources->clients_lock); + g_hash_table_foreach (sources->clients, gather_event_clients_cb, &list); + g_mutex_unlock (&sources->clients_lock); + + return list; +} + +gboolean +calendar_sources_has_clients (CalendarSources *sources) +{ + GHashTableIter iter; + gpointer value; + gboolean has = FALSE; + + g_return_val_if_fail (CALENDAR_IS_SOURCES (sources), FALSE); + + g_mutex_lock (&sources->clients_lock); + + g_hash_table_iter_init (&iter, sources->clients); + while (!has && g_hash_table_iter_next (&iter, NULL, &value)) + { + ClientData *cd = value; + + has = cd != NULL; + } + + g_mutex_unlock (&sources->clients_lock); + + return has; +} + +static void +backend_died_cb (EClient *client, + CalendarSources *sources) +{ + ESource *source; + const char *display_name; + + source = e_client_get_source (client); + display_name = e_source_get_display_name (source); + g_warning ("The calendar backend for '%s' has crashed.", display_name); + g_mutex_lock (&sources->clients_lock); + g_hash_table_remove (sources->clients, source); + g_mutex_unlock (&sources->clients_lock); +} + +static EClient * +calendar_sources_connect_client_sync (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GError **error) +{ + EClient *client = NULL; + ClientData *client_data; + + g_mutex_lock (&sources->clients_lock); + client_data = g_hash_table_lookup (sources->clients, source); + if (client_data) + client = E_CLIENT (g_object_ref (client_data->client)); + g_mutex_unlock (&sources->clients_lock); + + if (client) + return client; + + client = e_cal_client_connect_sync (source, source_type, wait_for_connected_seconds, cancellable, error); + if (!client) + return NULL; + + g_mutex_lock (&sources->clients_lock); + client_data = g_hash_table_lookup (sources->clients, source); + if (client_data) + { + g_clear_object (&client); + client = E_CLIENT (g_object_ref (client_data->client)); + } + else + { + client_data = g_new0 (ClientData, 1); + client_data->client = E_CAL_CLIENT (g_object_ref (client)); + client_data->backend_died_id = g_signal_connect (client, + "backend-died", + G_CALLBACK (backend_died_cb), + sources); + + g_hash_table_insert (sources->clients, g_object_ref (source), client_data); + } + g_mutex_unlock (&sources->clients_lock); + + return client; +} + +typedef struct _AsyncContext { + ESource *source; + ECalClientSourceType source_type; + guint32 wait_for_connected_seconds; +} AsyncContext; + +static void +async_context_free (gpointer ptr) +{ + AsyncContext *ctx = ptr; + + if (ctx) + { + g_clear_object (&ctx->source); + g_free (ctx); + } +} + +static void +calendar_sources_connect_client_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CalendarSources *sources = source_object; + AsyncContext *ctx = task_data; + EClient *client; + GError *local_error = NULL; + + client = calendar_sources_connect_client_sync (sources, ctx->source, ctx->source_type, + ctx->wait_for_connected_seconds, cancellable, &local_error); + if (!client) + { + if (local_error) + g_task_return_error (task, local_error); + else + g_task_return_pointer (task, NULL, NULL); + } else { + g_task_return_pointer (task, client, g_object_unref); + } +} + +void +calendar_sources_connect_client (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + AsyncContext *ctx; + g_autoptr (GTask) task = NULL; + + ctx = g_new0 (AsyncContext, 1); + ctx->source = g_object_ref (source); + ctx->source_type = source_type; + ctx->wait_for_connected_seconds = wait_for_connected_seconds; + + task = g_task_new (sources, cancellable, callback, user_data); + g_task_set_source_tag (task, calendar_sources_connect_client); + g_task_set_task_data (task, ctx, async_context_free); + + g_task_run_in_thread (task, calendar_sources_connect_client_thread); +} + +EClient * +calendar_sources_connect_client_finish (CalendarSources *sources, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, sources), NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, calendar_sources_connect_client), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + +void +print_debug (const gchar *format, + ...) +{ + g_autofree char *s = NULL; + g_autofree char *timestamp = NULL; + va_list ap; + g_autoptr (GDateTime) now = NULL; + static size_t once_init_value = 0; + static gboolean show_debug = FALSE; + static guint pid = 0; + + if (g_once_init_enter (&once_init_value)) + { + show_debug = (g_getenv ("CALENDAR_SERVER_DEBUG") != NULL); + pid = getpid (); + g_once_init_leave (&once_init_value, 1); + } + + if (!show_debug) + goto out; + + now = g_date_time_new_now_local (); + timestamp = g_date_time_format (now, "%H:%M:%S"); + + va_start (ap, format); + s = g_strdup_vprintf (format, ap); + va_end (ap); + + g_print (CALENDAR_SERVER_NAME "[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; +} diff --git a/calendar-server/calendar-sources.h b/calendar-server/calendar-sources.h new file mode 100644 index 000000000..f65f5a2f7 --- /dev/null +++ b/calendar-server/calendar-sources.h @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: + * Mark McLoughlin + * William Jon McCann + * Martin Grimme + * Christian Kellner + */ + +#ifndef __CALENDAR_SOURCES_H__ +#define __CALENDAR_SOURCES_H__ + +#include + +#define EDS_DISABLE_DEPRECATED +G_GNUC_BEGIN_IGNORE_DEPRECATIONS +#include +#include +G_GNUC_END_IGNORE_DEPRECATIONS + +G_BEGIN_DECLS + +#define CALENDAR_TYPE_SOURCES (calendar_sources_get_type ()) +G_DECLARE_FINAL_TYPE (CalendarSources, calendar_sources, + CALENDAR, SOURCES, GObject) + +CalendarSources *calendar_sources_get (void); +ESourceRegistry *calendar_sources_get_registry (CalendarSources *sources); +GSList *calendar_sources_ref_clients (CalendarSources *sources); +gboolean calendar_sources_has_clients (CalendarSources *sources); + +void calendar_sources_connect_client (CalendarSources *sources, + ESource *source, + ECalClientSourceType source_type, + guint32 wait_for_connected_seconds, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +EClient *calendar_sources_connect_client_finish + (CalendarSources *sources, + GAsyncResult *result, + GError **error); + +/* Set the environment variable CALENDAR_SERVER_DEBUG to show debug */ +void print_debug (const gchar *str, + ...) G_GNUC_PRINTF (1, 2); + +G_END_DECLS + +#endif /* __CALENDAR_SOURCES_H__ */ diff --git a/calendar-server/meson.build b/calendar-server/meson.build new file mode 100644 index 000000000..f4ccd14e8 --- /dev/null +++ b/calendar-server/meson.build @@ -0,0 +1,37 @@ +calendar_sources = [ + 'calendar-server.c', + 'calendar-debug.h', + 'calendar-sources.c', + 'calendar-sources.h', +] + + +calendar_server_name = meson.project_name() + '-calendar-server' +calendar_server = executable( + calendar_server_name, + calendar_sources, + dependencies: [ecal_dep, eds_dep, gio_dep], + include_directories: include_directories('..'), + c_args: [ + '-DPREFIX="@0@"'.format(prefix), + '-DLIBDIR="@0@"'.format(libdir), + '-DDATADIR="@0@"'.format(datadir), + '-DG_LOG_DOMAIN="ShellCalendarServer"', + '-DCALENDAR_SERVER_NAME="@0@"'.format(calendar_server_name), + ], + install_dir: libexecdir, + install: true, +) + +service_data = configuration_data() +service_data.set('app_id', app_id) +service_data.set('shell', meson.project_name()) +service_data.set('libexecdir', libexecdir) +service_file = 'CalendarServer.service' + +configure_file( + input: service_file + '.in', + output: app_id + '.' + service_file, + configuration: service_data, + install_dir: servicedir, +) diff --git a/data/00_mobi.Phosh.gschema.override b/data/00_mobi.Phosh.gschema.override new file mode 100644 index 000000000..64404292f --- /dev/null +++ b/data/00_mobi.Phosh.gschema.override @@ -0,0 +1,37 @@ +[org.gnome.desktop.a11y.applications:Phosh] +screen-keyboard-enabled=true + +[org.gnome.desktop.background:Phosh] +picture-uri='file:///usr/share/backgrounds/gnome/blobs-l.svg' +picture-uri-dark='file:///usr/share/backgrounds/gnome/blobs-d.svg' + +[org.gnome.desktop.interface:Phosh] +accent-color='yellow' +clock-show-date=false +clock-show-weekday=false +color-scheme='prefer-dark' + +[org.gnome.desktop.screensaver:Phosh] +picture-uri='file:///usr/share/backgrounds/gnome/blobs-d.svg' +picture-options='none' + +[org.gnome.desktop.session:Phosh] +idle-delay=60 + +[org.gnome.desktop.sound:Phosh] +theme-name='phosh' + +[org.gnome.settings-daemon.plugins.power:Phosh] +ambient-enabled=false +power-button-action='nothing' + +[org.gnome.settings-daemon.plugins.wwan:Phosh] +unlock-sim=true + +[sm.puri.phosh:Phosh] +# gnome-calendar: https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1697 +# Loupe and Showtime will be fixed with GNOME 49 +force-adaptive=['firefox-esr.desktop', 'org.gnome.Calendar.desktop', 'org.gnome.Loupe.desktop', 'org.gnome.Showtime.desktop'] + +[sm.puri.phosh.plugins:Phosh] +quick-settings=['wifi-hotspot-quick-setting', 'mobile-data-quick-setting'] \ No newline at end of file diff --git a/data/icons/app-close-symbolic.svg b/data/icons/app-close-symbolic.svg new file mode 100644 index 000000000..34f0823ca --- /dev/null +++ b/data/icons/app-close-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/app-icon-unknown-symbolic.svg b/data/icons/app-icon-unknown-symbolic.svg new file mode 100644 index 000000000..335663ee1 --- /dev/null +++ b/data/icons/app-icon-unknown-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/app-icon-unknown.svg b/data/icons/app-icon-unknown.svg new file mode 100644 index 000000000..36ad86d5c --- /dev/null +++ b/data/icons/app-icon-unknown.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/asterisk-symbolic.svg b/data/icons/asterisk-symbolic.svg new file mode 100644 index 000000000..5103f841f --- /dev/null +++ b/data/icons/asterisk-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/audio-handsfree-symbolic.svg b/data/icons/audio-handsfree-symbolic.svg new file mode 100644 index 000000000..c5a56fc66 --- /dev/null +++ b/data/icons/audio-handsfree-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/auth-sim-locked-symbolic.svg b/data/icons/auth-sim-locked-symbolic.svg new file mode 100644 index 000000000..e5c062fae --- /dev/null +++ b/data/icons/auth-sim-locked-symbolic.svg @@ -0,0 +1,36 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/auth-sim-missing-symbolic.svg b/data/icons/auth-sim-missing-symbolic.svg new file mode 100644 index 000000000..16165154b --- /dev/null +++ b/data/icons/auth-sim-missing-symbolic.svg @@ -0,0 +1,36 @@ + + + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + + + + + + diff --git a/data/icons/auto-brightness-symbolic.svg b/data/icons/auto-brightness-symbolic.svg new file mode 100644 index 000000000..58536ec1f --- /dev/null +++ b/data/icons/auto-brightness-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/camera-hardware-disabled-symbolic.svg b/data/icons/camera-hardware-disabled-symbolic.svg new file mode 100644 index 000000000..eabfe3648 --- /dev/null +++ b/data/icons/camera-hardware-disabled-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/chat-none-symbolic.svg b/data/icons/chat-none-symbolic.svg new file mode 100644 index 000000000..78660d6fe --- /dev/null +++ b/data/icons/chat-none-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/chat-symbolic.svg b/data/icons/chat-symbolic.svg new file mode 100644 index 000000000..17336b7fe --- /dev/null +++ b/data/icons/chat-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/eye-not-looking-symbolic.svg b/data/icons/eye-not-looking-symbolic.svg new file mode 100644 index 000000000..792a22ad8 --- /dev/null +++ b/data/icons/eye-not-looking-symbolic.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/data/icons/eye-open-negative-filled-symbolic.svg b/data/icons/eye-open-negative-filled-symbolic.svg new file mode 100644 index 000000000..f4e133a92 --- /dev/null +++ b/data/icons/eye-open-negative-filled-symbolic.svg @@ -0,0 +1,26 @@ + + + + + + image/svg+xml + + Gnome Symbolic Icon Theme + + + + Gnome Symbolic Icon Theme + + + + + + + + + + + + + + \ No newline at end of file diff --git a/data/icons/feedback-quiet-symbolic.svg b/data/icons/feedback-quiet-symbolic.svg new file mode 100644 index 000000000..c90187497 --- /dev/null +++ b/data/icons/feedback-quiet-symbolic.svg @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/data/icons/input-powerbar-symbolic.svg b/data/icons/input-powerbar-symbolic.svg new file mode 100644 index 000000000..9e1b42b5f --- /dev/null +++ b/data/icons/input-powerbar-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/meson.build b/data/icons/meson.build new file mode 100644 index 000000000..8046858aa --- /dev/null +++ b/data/icons/meson.build @@ -0,0 +1,4 @@ +install_data( + 'mobi.phosh.Shell-symbolic.svg', + install_dir: join_paths(datadir, 'icons', 'hicolor', 'symbolic', 'apps'), +) diff --git a/data/icons/microphone-hardware-disabled-symbolic.svg b/data/icons/microphone-hardware-disabled-symbolic.svg new file mode 100644 index 000000000..8da5ee8a4 --- /dev/null +++ b/data/icons/microphone-hardware-disabled-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/mobi.phosh.Shell-symbolic.svg b/data/icons/mobi.phosh.Shell-symbolic.svg new file mode 100644 index 000000000..20dc3504d --- /dev/null +++ b/data/icons/mobi.phosh.Shell-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/icons/mobile-data-disabled-symbolic.svg b/data/icons/mobile-data-disabled-symbolic.svg new file mode 100644 index 000000000..98ed58b23 --- /dev/null +++ b/data/icons/mobile-data-disabled-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/mobile-data-symbolic.svg b/data/icons/mobile-data-symbolic.svg new file mode 100644 index 000000000..ff106c814 --- /dev/null +++ b/data/icons/mobile-data-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/moon-filled-symbolic.svg b/data/icons/moon-filled-symbolic.svg new file mode 100644 index 000000000..f1a7b59d7 --- /dev/null +++ b/data/icons/moon-filled-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/network-cellular-disabled-symbolic.svg b/data/icons/network-cellular-disabled-symbolic.svg new file mode 100644 index 000000000..4a70eef3f --- /dev/null +++ b/data/icons/network-cellular-disabled-symbolic.svg @@ -0,0 +1,66 @@ + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/data/icons/network-cellular-no-data-signal-excellent-symbolic.svg b/data/icons/network-cellular-no-data-signal-excellent-symbolic.svg new file mode 100644 index 000000000..1c948c8d8 --- /dev/null +++ b/data/icons/network-cellular-no-data-signal-excellent-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/icons/network-cellular-no-data-signal-good-symbolic.svg b/data/icons/network-cellular-no-data-signal-good-symbolic.svg new file mode 100644 index 000000000..3e305b99c --- /dev/null +++ b/data/icons/network-cellular-no-data-signal-good-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/network-cellular-no-data-signal-none-symbolic.svg b/data/icons/network-cellular-no-data-signal-none-symbolic.svg new file mode 100644 index 000000000..f7eab457e --- /dev/null +++ b/data/icons/network-cellular-no-data-signal-none-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/data/icons/network-cellular-no-data-signal-ok-symbolic.svg b/data/icons/network-cellular-no-data-signal-ok-symbolic.svg new file mode 100644 index 000000000..993e125d3 --- /dev/null +++ b/data/icons/network-cellular-no-data-signal-ok-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/network-cellular-no-data-signal-weak-symbolic.svg b/data/icons/network-cellular-no-data-signal-weak-symbolic.svg new file mode 100644 index 000000000..5a56fdf30 --- /dev/null +++ b/data/icons/network-cellular-no-data-signal-weak-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/network-vpn-disabled-symbolic.svg b/data/icons/network-vpn-disabled-symbolic.svg new file mode 100644 index 000000000..18af78d01 --- /dev/null +++ b/data/icons/network-vpn-disabled-symbolic.svg @@ -0,0 +1,19 @@ + + + + + + diff --git a/data/icons/network-wireless-disabled-symbolic.svg b/data/icons/network-wireless-disabled-symbolic.svg new file mode 100644 index 000000000..fa75c652f --- /dev/null +++ b/data/icons/network-wireless-disabled-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/data/icons/no-notifications-symbolic.svg b/data/icons/no-notifications-symbolic.svg new file mode 100644 index 000000000..d30f7e1f6 --- /dev/null +++ b/data/icons/no-notifications-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/padlock-symbolic.svg b/data/icons/padlock-symbolic.svg new file mode 100644 index 000000000..b5f51b7ff --- /dev/null +++ b/data/icons/padlock-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/phone-docked-symbolic.svg b/data/icons/phone-docked-symbolic.svg new file mode 100644 index 000000000..c873598e0 --- /dev/null +++ b/data/icons/phone-docked-symbolic.svg @@ -0,0 +1,50 @@ + + + + + + image/svg+xml + + + + + + + + phonelink + + + + + diff --git a/data/icons/phone-undocked-symbolic.svg b/data/icons/phone-undocked-symbolic.svg new file mode 100644 index 000000000..99f9b1c89 --- /dev/null +++ b/data/icons/phone-undocked-symbolic.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/data/icons/screen-rotation-landscape-symbolic.svg b/data/icons/screen-rotation-landscape-symbolic.svg new file mode 100644 index 000000000..2a88e0a83 --- /dev/null +++ b/data/icons/screen-rotation-landscape-symbolic.svg @@ -0,0 +1,68 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/screen-rotation-portrait-symbolic.svg b/data/icons/screen-rotation-portrait-symbolic.svg new file mode 100644 index 000000000..0564f2243 --- /dev/null +++ b/data/icons/screen-rotation-portrait-symbolic.svg @@ -0,0 +1,68 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/data/icons/screenshot-portrait-symbolic.svg b/data/icons/screenshot-portrait-symbolic.svg new file mode 100644 index 000000000..b9bd24880 --- /dev/null +++ b/data/icons/screenshot-portrait-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/data/icons/settings-symbolic.svg b/data/icons/settings-symbolic.svg new file mode 100644 index 000000000..408d7e5d4 --- /dev/null +++ b/data/icons/settings-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/data/icons/skip-backwards-10-symbolic.svg b/data/icons/skip-backwards-10-symbolic.svg new file mode 100644 index 000000000..d8ae3de20 --- /dev/null +++ b/data/icons/skip-backwards-10-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/skip-forward-30-symbolic.svg b/data/icons/skip-forward-30-symbolic.svg new file mode 100644 index 000000000..d57bb5ed1 --- /dev/null +++ b/data/icons/skip-forward-30-symbolic.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/data/icons/splash-process-working-symbolic.svg b/data/icons/splash-process-working-symbolic.svg new file mode 100644 index 000000000..9de62c1db --- /dev/null +++ b/data/icons/splash-process-working-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/data/icons/swipe-arrow-symbolic.svg b/data/icons/swipe-arrow-symbolic.svg new file mode 100644 index 000000000..c3d66ec65 --- /dev/null +++ b/data/icons/swipe-arrow-symbolic.svg @@ -0,0 +1,35 @@ + + + + + + + image/svg+xml + + + + + + + + + diff --git a/data/icons/torch-disabled-symbolic.svg b/data/icons/torch-disabled-symbolic.svg new file mode 100644 index 000000000..879f85243 --- /dev/null +++ b/data/icons/torch-disabled-symbolic.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/data/icons/torch-enabled-symbolic.svg b/data/icons/torch-enabled-symbolic.svg new file mode 100644 index 000000000..0cb5a0682 --- /dev/null +++ b/data/icons/torch-enabled-symbolic.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/data/leak-suppress.txt b/data/leak-suppress.txt new file mode 100644 index 000000000..736d3cddd --- /dev/null +++ b/data/leak-suppress.txt @@ -0,0 +1,30 @@ +# Use via environment variable LSAN_OPTIONS=suppressions=data/leak-suppress.txt + +# Ignore fontconfig reported leaks. It's caches cause false positives. +leak:libfontconfig.so.1 + +# +# Glib +# +# Quarks stay around +leak:g_quark_init + +# +# Gvfs: +# +# https://gitlab.gnome.org/GNOME/gvfs/-/issues/577 +leak:g_daemon_vfs_init + +# +# GTK3/GDK +# +# gtk's gdk_set_allowed_backend uses g_strdup() +leak:gdk_set_allowed_backends +# Displays from gtk's gdk_display_manager_open_display are never free'd +leak:gdk_display_manager_open_display +# See gtk-style-context suppressions in gtk's gtk.supp: +leak:gtk_css_node_declaration_make_writable +leak:gtk_css_widget_node_new +# GDK leaks here. Needs investigation. +leak:gdk_window_init +leak:_gdk_wayland_display_create_window_impl diff --git a/data/meson.build b/data/meson.build new file mode 100644 index 000000000..da2e4fa8f --- /dev/null +++ b/data/meson.build @@ -0,0 +1,154 @@ +gsd_required_components = [ + 'org.gnome.SettingsDaemon.A11ySettings', + 'org.gnome.SettingsDaemon.Color', + 'org.gnome.SettingsDaemon.Datetime', + 'org.gnome.SettingsDaemon.Housekeeping', + 'org.gnome.SettingsDaemon.Keyboard', + 'org.gnome.SettingsDaemon.MediaKeys', + 'org.gnome.SettingsDaemon.Power', + 'org.gnome.SettingsDaemon.PrintNotifications', + 'org.gnome.SettingsDaemon.Rfkill', + 'org.gnome.SettingsDaemon.ScreensaverProxy', + 'org.gnome.SettingsDaemon.Sharing', + 'org.gnome.SettingsDaemon.Smartcard', + 'org.gnome.SettingsDaemon.Sound', + 'org.gnome.SettingsDaemon.UsbProtection', + 'org.gnome.SettingsDaemon.Wacom', + 'org.gnome.SettingsDaemon.Wwan', +] + +desktop_required_components = gsd_required_components + [ + 'mobi.phosh.Shell', + 'sm.puri.OSK0', +] + +desktopconf = configuration_data() +desktopconf.set('bindir', bindir) +desktopconf.set('libexecdir', libexecdir) +desktopconf.set( + 'required_components', + ';'.join(desktop_required_components) + ';', +) + +desktop_utils = find_program('desktop-file-validate', required: false) +desktop_files = {'mobi.phosh.Shell.desktop': true} +foreach desktop_file, install : desktop_files + merged = i18n.merge_file( + input: configure_file( + input: desktop_file + '.in.in', + output: desktop_file + '.in', + configuration: desktopconf, + ), + output: desktop_file, + po_dir: '../po', + install: true, + install_dir: desktopdir, + type: 'desktop', + ) + + if desktop_utils.found() + test('Validate desktop file', desktop_utils, args: [merged], suite: 'tools') + endif +endforeach + +sessions = ['phosh.session'] +foreach session : sessions + desktop = session + '.desktop' + i18n.merge_file( + input: configure_file( + input: desktop + '.in.in', + output: desktop + '.in', + configuration: desktopconf, + ), + output: session, + po_dir: '../po', + install: true, + install_dir: join_paths(sessiondir, 'sessions'), + type: 'desktop', + ) +endforeach + +runconf = configuration_data() +runconf.set('bindir', bindir) +runconf.set('libexecdir', libexecdir) +runconf.set('pkgdatadir', pkgdatadir) +runconf.set('version', meson.project_version()) +runconf.set('wlrootsdir', join_paths(libexecdir, 'wlroots')) +runconf.set('compositor', get_option('compositor')) + +configure_file( + input: 'phosh-session.in', + output: 'phosh-session', + install_dir: bindir, + configuration: runconf, + install: true, +) + +# Generate XML enum definitions for GSettings schema +schema_enum_headers = files('..' / 'src' / 'phosh-settings-enums.h') +generate_enums_schema = gnome.mkenums( + 'mobi.phosh.shell.enums.xml', + sources: schema_enum_headers, + comments: '', + fhead: '', + vhead: ' <@type@ id="mobi.phosh.shell.@EnumName@">', + vprod: ' ', + vtail: ' ', + ftail: '', + install_header: true, + install_dir: schemasdir, +) + +#workaround due to https://github.com/mesonbuild/meson/issues/1687 +copy_schema = custom_target( + 'copy-gschema-to-builddir', + input: 'mobi.phosh.shell.gschema.xml', + output: 'mobi.phosh.shell.gschema.xml', + command: ['cp', '@INPUT@', '@OUTPUT@'], +) +schemas = ['mobi.phosh.shell.gschema.xml'] +compiled_schemas = custom_target( + 'glib-compile-schemas', + build_by_default: true, + output: 'gschemas.compiled', + install: false, + command: [find_program('glib-compile-schemas'), meson.current_build_dir()], + depends: [generate_enums_schema, copy_schema], +) + +overrides = ['00_mobi.Phosh.gschema.override'] + +install_data(schemas + overrides, install_dir: schemasdir) + +subdir('systemd') +subdir('icons') + +install_data('phoc.ini', install_dir: pkgdatadir) +install_data( + 'wayland-sessions/phosh.desktop', + install_dir: 'share/wayland-sessions', +) + +install_data( + 'phosh-shell.portal', + install_dir: join_paths(datadir, 'xdg-desktop-portal', 'portals'), +) +install_data( + 'phosh-portals.conf', + install_dir: join_paths(datadir, 'xdg-desktop-portal'), +) + +shell_search_libexecdir = join_paths(prefix, get_option('libexecdir')) + +serviceconf = configuration_data() +serviceconf.set('libexecdir', shell_search_libexecdir) + +if get_option('searchd') + configure_file( + input: 'mobi.phosh.Shell.Search.service.in', + output: 'mobi.phosh.Shell.Search.service', + install_dir: servicedir, + configuration: serviceconf, + install: true, + ) +endif diff --git a/data/mobi.phosh.Shell.Search.service.in b/data/mobi.phosh.Shell.Search.service.in new file mode 100644 index 000000000..06c160906 --- /dev/null +++ b/data/mobi.phosh.Shell.Search.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=mobi.phosh.Shell.Search +Exec=@libexecdir@/phosh-searchd --gapplication-service diff --git a/data/mobi.phosh.Shell.desktop.in.in b/data/mobi.phosh.Shell.desktop.in.in new file mode 100644 index 000000000..981ad9195 --- /dev/null +++ b/data/mobi.phosh.Shell.desktop.in.in @@ -0,0 +1,15 @@ +[Desktop Entry] +Type=Application +Name=Phone Shell +Comment=Window management and application launching for mobile +Exec=@libexecdir@/phosh +Categories=System;GNOME;GTK;Core; +OnlyShowIn=GNOME; +NoDisplay=true +Icon=mobi.phosh.Shell +X-GNOME-Autostart-Phase=DisplayServer +X-GNOME-Provides=panel;windowmanager; +X-GNOME-Autostart-Notify=true +X-GNOME-AutoRestart=true +X-GNOME-HiddenUnderSystemd=true +X-Phosh-UsesFeedback=true diff --git a/data/mobi.phosh.shell.gschema.xml b/data/mobi.phosh.shell.gschema.xml new file mode 100644 index 000000000..0df34d8dc --- /dev/null +++ b/data/mobi.phosh.shell.gschema.xml @@ -0,0 +1,212 @@ + + + + + true + Whether cell broadcast messages should be displayed + + When this is disabled the shell will not show any cell + broadcast messages, including important security warnings. + + + + + + + 0.0 + + Auto brightness offset + + Fixed offset added to the calculated auto brightnes value. + + + + + + + + + [ 'org.gnome.Calls.desktop', + 'sm.puri.Chatty.desktop', + 'org.gnome.Epiphany.desktop', + 'org.gnome.Contacts.desktop' + ] + List of desktop file IDs for favorite applications + + The applications corresponding to these identifiers will be + displayed in the favorites panel along with running applications. + + + + + ['adaptive'] + + How to filter apps in the overview. The default 'adaptive' shows only + adaptive apps in mobile mode and all apps in docked mode. 'off' turns + of all filtering. + + + Whether to filter out apps that aren't marked as adaptive in mobile mode. + + + + + [] + + List of desktop file IDs for applications that are adaptive for phone + usage but not marked as such in their desktop file. + + + The applications corresponding to these identifiers will be + displayed in the app grid panel even when in mobile mode that only + lists touch capable apps that adapt to small screen sizes. + + + + + 'modemmanager' + Which backend to use for interfacing with the cellular modem + + The available backends are 'modemmanager' for ModemManager (the default) + and 'ofono' for oFono. This setting is only read when Phosh starts, so a + shell restart is required after changing it. + + + + + false + Whether to switch to HighContrast theme in high brightness conditions + + Setting this to true enables automatic switching to HighContrast on high ambient + night levels. + + + + + 500 + The light level (in Lux) to switch to HighContrast theme + + When automatic high contrast is enabled this defines the light level at which to switch. + + + + + false + Whether to enable suspend from the power menu + + Setting this to true allows manually suspending the device from the power menu. + Suspend is an experimental feature on some devices and may result in crashes or instability. + + + + + false + Allow to quickly silence the device + + Setting this to true allows to set the phone to silent by + pressing volume-down on incoming calls when the phone is locked. + This feature is experimental. + + + + + 'device' + What information to use to layout UI elements. + + This determines if Phosh will use device information to layout + UI elements in order to not overlap with notches and cutouts. + + + + + 1.0 + OSK unfold long press delay factor + + Delay factor for long press on the home bar to unfold the keyboard. + Valid values are [0.5,2.0]. Values less than 1.0 make the unfold trigger + sooner, greater than 1.0 make the unfold trigger later. + + + + + + + false + Whether emergency calls are enabled + + When this is disabled the shell will not offer emergency call + functionality. + + + + + + + false + Whether to scramble the keypad + + Setting this to true shuffle the digits on the PIN keypad to + make eavesdropping harder. + + + + + true + Require authentication in lockscreen + + Setting this to false allows unlocking without entering the PIN + or password by simply swiping up. + + + + + + + + + + + + + ['urgency'] + What notification properties wake up the screen + + This setting defines what properties of a notification wake up the screen. (e.g. the + notification's category or urgency). + + + + [] + Notification categories that wake up the screen + + If matching by category is enabled defines which categories + will wake up the screen. + + + + 'critical' + Notification urgency that wakes up the screen + + If matching by urgency is enabled sets the lowest notification + urgency that wakes up the screen. + + + + + + + [] + List of enabled lockscreen plugins + + The list of currently enabled plugins on the lock screen. + + + + [] + List of enabled quick setting plugins + + The list of currently enabled plugins on the quick settings box. + + + + diff --git a/data/phoc.ini b/data/phoc.ini new file mode 100644 index 000000000..786a4c519 --- /dev/null +++ b/data/phoc.ini @@ -0,0 +1,26 @@ +#[core] +#xwayland=false + +#[output:DSI-1] +#scale = 2 + +[output:Virtual-1] +# For the x86 VM using QXL to get a phone like geometry +modeline = 87.25 720 776 848 976 1440 1443 1453 1493 -hsync +vsync +mode = 720x1440 +scale = 2 + +[output:X11-1] +mode = 360x720 +#rotate = 90 +#scale = 1 + +[output:WL-1] +mode = 360x720 +#rotate = 90 +#scale = 1 + +[output:HEADLESS-1] +mode = 720x1440 +#rotate = 90 +scale = 2 diff --git a/data/phosh-portals.conf b/data/phosh-portals.conf new file mode 100644 index 000000000..3bf377d27 --- /dev/null +++ b/data/phosh-portals.conf @@ -0,0 +1,12 @@ +[preferred] +default=phrosh;phosh;gtk; +org.freedesktop.impl.portal.Access=phosh-shell; +org.freedesktop.impl.portal.Background=none; +org.freedesktop.impl.portal.Clipboard=none; +org.freedesktop.impl.portal.GlobalShortcuts=none; +org.freedesktop.impl.portal.InputCapture=none; +org.freedesktop.impl.portal.RemoteDesktop=none; +org.freedesktop.impl.portal.ScreenCast=wlr; +org.freedesktop.impl.portal.Screenshot=gtk;wlr; +org.freedesktop.impl.portal.Secret=gnome-keyring; +org.freedesktop.impl.portal.Usb=none; diff --git a/data/phosh-session.in b/data/phosh-session.in new file mode 100755 index 000000000..960eb5e7a --- /dev/null +++ b/data/phosh-session.in @@ -0,0 +1,55 @@ +#!/bin/sh + +COMPOSITOR="@compositor@" +PHOC_INI="@pkgdatadir@/phoc.ini" +PHOSH_SESSION_BIN=${PHOSH_SESSION_BIN:-gnome-session} + +help() +{ + cat </dev/null 2>&1; then + SYSTEMD_CAT="systemd-cat -t phoc" +fi + +[ -n "$WLR_BACKENDS" ] || WLR_BACKENDS=drm,libinput +export WLR_BACKENDS +exec $SYSTEMD_CAT "${COMPOSITOR}" -v -S -C "${PHOC_INI}" -E "${PHOSH_SESSION_BIN} --session=phosh" diff --git a/data/phosh-shell.portal b/data/phosh-shell.portal new file mode 100644 index 000000000..1c67fa7d5 --- /dev/null +++ b/data/phosh-shell.portal @@ -0,0 +1,4 @@ +[portal] +DBusName=mobi.phosh.Shell.Portal +Interfaces=org.freedesktop.impl.portal.Access +UseIn=phosh diff --git a/data/phosh.service b/data/phosh.service new file mode 100644 index 000000000..ad34c5f23 --- /dev/null +++ b/data/phosh.service @@ -0,0 +1,62 @@ +[Unit] +Description=Phosh, a shell for mobile phones (Development service) +Documentation=https://gitlab.gnome.org/World/Phosh/phosh + +# replaces the getty +Conflicts=getty@tty1.service +After=getty@tty1.service + +# Needs all the dependencies of the services it's replacing +# (currently getty@tty1.service): +After=rc-local.service plymouth-quit-wait.service systemd-user-sessions.service + +OnFailure=getty@tty1.service + +# D-Bus is necessary for contacting logind. Logind is required. +Wants=dbus.socket +After=dbus.socket + +# This scope is created by pam_systemd when logging in as the user. +# This directive is a workaround to a systemd bug, where the setup of the +# user session by PAM has some race condition, possibly leading to a failure. +# See README for more details. +After=session-c1.scope + +# Since we are part of the graphical session, make sure we are started before +# it is complete. +Before=graphical.target + +# Prevent starting on systems without virtual consoles +ConditionPathExists=/dev/tty0 + +[Service] +Environment=LANG=C.UTF-8 +Environment=XDG_CURRENT_DESKTOP=Phosh:GNOME +Environment=XDG_SESSION_DESKTOP=phosh +Environment=XDG_SESSION_TYPE=wayland +ExecStart=capsh --noamb -- -c "exec /usr/bin/phosh-session" +ExecStartPost=+chvt 7 +TimeoutStartSec=30 +User=1000 +PAMName=login +WorkingDirectory=~ +Restart=always +RestartSec=5s + +# A virtual terminal is needed. +TTYPath=/dev/tty7 +TTYReset=yes +TTYVHangup=yes +TTYVTDisallocate=yes + +# Fail to start if not controlling the tty. +StandardInput=tty-fail +StandardOutput=journal +StandardError=journal + +# Log this user with utmp, letting it show up with commands 'w' and 'who'. +UtmpIdentifier=tty7 +UtmpMode=user + +[Install] +WantedBy=graphical.target diff --git a/data/phosh.session.desktop.in.in b/data/phosh.session.desktop.in.in new file mode 100644 index 000000000..507869fe4 --- /dev/null +++ b/data/phosh.session.desktop.in.in @@ -0,0 +1,4 @@ +[GNOME Session] +# Translators: this is the session name, no need to translate it +Name=Phosh +RequiredComponents=@required_components@ diff --git a/data/systemd/meson.build b/data/systemd/meson.build new file mode 100644 index 000000000..0db1003cc --- /dev/null +++ b/data/systemd/meson.build @@ -0,0 +1,34 @@ +gsd_wants = '' +foreach component : gsd_required_components + gsd_wants += 'Wants=' + component + '.target\n' +endforeach + +session_dropins = ['gnome-session@phosh.target.d'] + +sessionconf = configuration_data() +sessionconf.set('gsd_wants', gsd_wants) +sessionconf.set('app_id', app_id) + +foreach session_dropin : session_dropins + configure_file( + input: 'phosh.session.conf.in', + output: 'session.conf', + install_dir: join_paths(systemduserdir, session_dropin), + configuration: sessionconf, + install: true, + ) +endforeach + +serviceconf = configuration_data() +serviceconf.set('libexecdir', libexecdir) +configure_file( + input: 'mobi.phosh.Shell.service.in', + output: 'mobi.phosh.Shell.service', + install_dir: systemduserdir, + configuration: serviceconf, + install: true, +) + +foreach type : ['Shell', 'OSK'] + install_data(f'mobi.phosh.@type@.target', install_dir: systemduserdir) +endforeach diff --git a/data/systemd/mobi.phosh.OSK.target b/data/systemd/mobi.phosh.OSK.target new file mode 100644 index 000000000..d343676f1 --- /dev/null +++ b/data/systemd/mobi.phosh.OSK.target @@ -0,0 +1,11 @@ +[Unit] +Description=Phosh On Screen Keyboard +CollectMode=inactive-or-failed + +# Pull in the service +Wants=mobi.phosh.OSK.service + +# Require GNOME session and specify startup ordering +Requisite=gnome-session-initialized.target +After=mobi.phosh.Shell.target +After=mobi.phosh.Phrog.target diff --git a/data/systemd/mobi.phosh.Shell.service.in b/data/systemd/mobi.phosh.Shell.service.in new file mode 100644 index 000000000..57f4a5253 --- /dev/null +++ b/data/systemd/mobi.phosh.Shell.service.in @@ -0,0 +1,27 @@ +# This is a systemd user unit intended to be started with gnome-session. + +[Unit] +Description=Phosh, a shell for mobile phones +Documentation=https://gitlab.gnome.org/World/Phosh/phosh + +After=gnome-session-manager.target + +Requisite=gnome-session-initialized.target +PartOf=gnome-session-initialized.target +Before=gnome-session-initialized.target + +StartLimitIntervalSec=15s +StartLimitBurst=3 + +OnFailure=gnome-session-shutdown.target +OnFailureJobMode=replace-irreversibly +CollectMode=inactive-or-failed +RefuseManualStart=on +RefuseManualStop=on + +[Service] +Type=notify +ExecStart=@libexecdir@/phosh +Restart=on-failure +Slice=session.slice +OOMScoreAdjust=-1000 diff --git a/data/systemd/mobi.phosh.Shell.target b/data/systemd/mobi.phosh.Shell.target new file mode 100644 index 000000000..fd261e0b4 --- /dev/null +++ b/data/systemd/mobi.phosh.Shell.target @@ -0,0 +1,10 @@ +[Unit] +Description=Phosh +DefaultDependencies=no + +Requisite=gnome-session-initialized.target +PartOf=gnome-session-initialized.target +Before=gnome-session-initialized.target + +Requires=mobi.phosh.Shell.service +After=mobi.phosh.Shell.service diff --git a/data/systemd/phosh.session.conf.in b/data/systemd/phosh.session.conf.in new file mode 100644 index 000000000..c5e55ca65 --- /dev/null +++ b/data/systemd/phosh.session.conf.in @@ -0,0 +1,6 @@ +[Unit] +@gsd_wants@ + +Wants=mobi.phosh.OSK.target + +Requires=@app_id@.target diff --git a/data/valgrind.suppressions b/data/valgrind.suppressions new file mode 100644 index 000000000..c1d924116 --- /dev/null +++ b/data/valgrind.suppressions @@ -0,0 +1,62 @@ +# Fontconfig +{ + css_fontconfig_FcPatternCreate + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + fun:FcPatternCreate + ... + fun:XML_ParseBuffer + ... + fun:gtk_label_get_preferred_layout_size + fun:gtk_label_get_preferred_size + fun:gtk_label_measure + fun:gtk_css_custom_gadget_get_preferred_size + fun:gtk_css_gadget_get_preferred_size + ... +} + +{ + css_fontconfig_FcFontRenderPrepare + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + ... + fun:FcFontRenderPrepare + fun:FcFontMatch + ... + fun:gtk_css_custom_gadget_get_preferred_size + fun:gtk_css_gadget_get_preferred_size + ... +} + +{ + fontconfig_malloc + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + obj:/usr/lib/*/libfontconfig.so.* + ... +} + +{ + fontconfig_realloc + Memcheck:Leak + match-leak-kinds: definite + fun:realloc + obj:/usr/lib/*/libfontconfig.so.* + ... +} + +# Temporary until +# https://gitlab.gnome.org/GNOME/dconf/merge_requests/25 +# is fixed +{ + dconf_OutstandingWatch_leak + Memcheck:Leak + match-leak-kinds: definite + fun:malloc + ... + fun:dconf_engine_watch_fast + ... +} diff --git a/data/wayland-sessions/phosh.desktop b/data/wayland-sessions/phosh.desktop new file mode 100644 index 000000000..28499aa8d --- /dev/null +++ b/data/wayland-sessions/phosh.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Phosh +Comment=Phone Shell +Comment=This session logs you into Phosh +Exec=phosh-session +Type=Application +DesktopNames=Phosh;GNOME; diff --git a/debian/README.source b/debian/README.source new file mode 100644 index 000000000..90e1f00d7 --- /dev/null +++ b/debian/README.source @@ -0,0 +1,29 @@ +This package is maintained with git-buildpackage(1). It follows DEP-14 +for branch naming (e.g. using debian/sid for the current version +in Debian unstable). + +It uses pristine-tar(1) to store enough information in git to generate +bit identical tarballs when building the package without having +downloaded an upstream tarball first. + +When working with patches it is recommended to use "gbp pq import" to +import the patches, modify the source and then use "gbp pq export +--commit" to commit the modifications. + +The changelog is generated using "gbp dch" so if you submit any +changes don't bother to add changelog entries but rather provide +a nice git commit message that can then end up in the changelog. + +It is recommended to build the package with pbuilder using: + + gbp buildpackage --git-pbuilder + +For information on how to set up a pbuilder environment see the +git-pbuilder(1) manpage. In short: + + DIST=sid git-pbuilder create + gbp clone + cd + gbp buildpackage --git-pbuilder + + -- Guido Günther , Wed, 2 Dec 2015 18:51:15 +0100 diff --git a/debian/changelog b/debian/changelog new file mode 100644 index 000000000..4ffa9620a --- /dev/null +++ b/debian/changelog @@ -0,0 +1,12885 @@ +phosh (0.53.0) experimental; urgency=medium + + [ Guido Günther ] + * portal: Use Rust based GUI variants by default. We want to drop the C + variants so let's switch. + * gvc: Update to latest upstream commit. We landed another bugfix, let's + grab it + + [ Álvaro Burns ] + * Update Brazilian Portuguese translation + + [ Quentin PAGÈS ] + * Update Occitan translation + + -- Guido Günther Sun, 15 Feb 2026 09:23:10 +0100 + +phosh (0.53~rc1) experimental; urgency=medium + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Guido Günther ] + * run: Use plugin_schema_path. + We have it already so no need to maintain it in two locations + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1298o + * util: Allow to activate without actions + * util: Make platform-data helpers public. We'll use them for + notifications + * util: Determine object path automatically. + Code taken from xdg-desktop-portal-phosh + * notification: Activate applications when sending notifications. + We activate the app and send the signal. + Should our heuristic to determine the bus name fail to work well we + can send `org.freedesktop.Notifications.ActivationToken` in + `on_app_activated` as a future improvement. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/852 + * doc: Document missing plugin schema entries + * brightness-manager: Use _ID for schema consistently. + No point in having one schema with that suffix and the other without it. + * brightness-manager: Use faster up interval. + The human eye adjust to brightness gains more rapidly. + * backlight: Round backlight level. + This gives us more precise values than integer cutoff. + * brightness-manager: Simplify ending the transition + * brightness-manager: Smoothen brightness transition. + Use smoothstep to make things look less jittering + * monitor-manager: Add night light temperature property. + This allows us to track whether it is in use + * brightness-manager: Add additional factor when night light is on. + Night light reduces contrast as the eye is less susceptible to contrast + in the red spectrum. Lets make the display a bit brighter in those + cases. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1292 + * brightness-manager: Drop schema check. We can assume the 49 schema + now. + * brightness-manager: Add auto-brightness offset. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1297 + * util: Fix indent. + Gbp-dch: Ignore + * app-tracker: Fix indent. + We shorten a variable name and add get the shell variable in an extra + step for readability. + * app-list-model: Allow app-info lookup by recently launched executable. + The shell is usually used to launch apps so when launching remember the + executable. This will allow us look up app-info by exectable. + This is useful for DBus activated apps too as they still usually `Exec` + have an exec line in the desktop file. + * audio-manager: Manage audio devices. + Track the input and output audio devices + * audio-settings: Use audio-devices and mixer control from audio-manager. + This will allow us to use audio devices outside settings + * audio-devices: Move out of settings. + They're used in the audio manager now + * channel-bar: Use proper namespace. + It's a widget in Phosh so use that namespace. This also removes some + confusion between the gvc submodule and our internal widget. + * channel-bar: Rename source files. + Do that in an extra commit to ease review + * audio-settings: Avoid channel-bar casts. + Just use the most specific type right away. + * audio-settings: Avoid pointless cast + * channel-bar: Remove unused defines + * audio-devices: Use type. + We used gpointer to not leak it into libphosh but the library + is more self contained nowadays. + * overview: Use common pattern for find_activity_by_toplevel() + Return early from the loop instead of breaking and make the `l->data` + assignment the first thing in the loop. + Also make sure `g_autoptr ()` is always initialized. + Again fewer lines with easier to read code. + * overview: Use most specific type for carousel. + Avoids the long casts + * activity: Just use `MIN` + No need to open code this. + * activity: Use phosh_util_toggle_style_class consistently. + We use it to remove the class, use it for adding it too + * shell: Create splash manager earlier. + We want to use it from the overview so it has to be there with the + panels. + * shell: Allow to get splash manager + * swipe-tracker: Allow to disable swipes + * splash: Allow to lower/raise the splash + * splash-manager: Allow to lower all splashs and to raise base on startup-id + * overview: Use splash colors instead of fake app. + The fake app wasn't very useful. Use the splash screen + colors instead. + * activity: Show spinner when we don't have a thumbnail. + Add a spinner and show it when we don't have a thumbnail of the + application yet. + * activity: Disable swipe and close button while app is loading. + We can't stop it sensibly atm. + * activity: Try harder to get an app-id + * overview: Show activity for launching apps. + This gives the user feedback that the app is launching. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/647 + * overview: Add splash manager + * overview: Raise and lower launch splashs. + When activating a splash we raise it to the top, when activating + an activity we lower all splashs so the app becomes visible. + This is not the ideal place to do it as it doesn't concernt the overview + but we track the toplevels here only atm (see + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/327) + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/812 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/813 + * overview: Use light mode based on what the splash manager does. + We won't switch when the user flips the theme during launch but + that's o.k. for the moment. + * activity: Render empty state only on drawing area. + This makes the thumbnail look more like the app + * activity: Use slightly rounded corners + * phosh-session: Drop disable-acceleration-check. + It is a noop nowadays + * phosh-session: Avoid the extra shell exec + gnome-session handles that for us + * phosh-session: Use name variable. + This ensures we don't accidentally clash with something set + in the environment + * media-player: Avoid nan when track length is empty. + Zero track length (as happens with gapless) can lead to -NAN + which trips up the progress bar. Use 0.0 in that case. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1287 + * brightness-manager: Skip brightenss update. + We can skip the brightness update on night light changes when + auto brightness is disabled. + * lockscreen: Sort callbacks alphabetically + * lockscreen: Use two finger swipe gesture for brightness. + This works better then trying to fit a zoom gesture. Move + to the UI file while at that. + Fixes: 336929ca4 ("lockscreen: Ignore gesture when user wants to zoom") + Co-authored-by: Arun Mani J + * packaging: Update dependencies. + We need recent schema to start successfully. Drop an ancient + conflict on calls and require stevia so we always have a working + OSK (user can still switch ofc). + * docs: Mention UsesNotifications. + This is is similar to UsesFeedback. + * gvc: Switch back to blessed repo. + Most of our patches are in. The leak fix is still missing but hopefully + we get that soon. + Update to 664eba4 ("mixer-control: Document that add_stream is transfer full") + * overview: Ignore missing activity. + When an app failed to start and we can't find a matching + activity that is usually because it crashed or exited + already. Avoid a critical in that case. + * toplevel-manager: Drop super-cautious checks. + The pointer arrays are created in init and never unref'ed or + free'd until dispose so no need to check this every call. + * overview: Drop unused arguments from signal handler. + No need to check types we never use + * toplevel-manager: Track launched apps. + When an app is reporting that it launced we track its app-id + and wait for a matching toplevel in toplevel-manager. When + that doesn't happen in time, we send out a signal. + * overview: Remove splash if we never get a toplevel. + In the unlikely case that we don't see a toplevel from a launched + app, remove the splash when toplevel-manager tells us that the + toplevel will likely never show up. + + [ Jürgen Benvenuti ] + * Update German translation + + [ Fredrik Fyksen ] + * Update Norwegian Bokmål translation + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + [ Flynn Peck ] + * Add Cornish translation + + [ Arun Mani J ] + * pokit-auth-agent: Follow our coding style + * polkit-auth-prompt: Follow our coding style + * polkit-auth-agent: Uncrustify + * polkit-auth-prompt: Uncrustify + * polkit-auth-prompt: Display more information about user. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1229 + + [ Rudra Pratap Singh ] + * top-panel: Add a common parsing function for panel options. + This way, we can keep the parsing in + sync for both mobile and non mobile panels. + * top-panel: Make panel launching actions take parameters. + Instead of a string we use (sav) parameter type + so we can pass additional parameters. + * util: Allow to open settings panel with additional parameters. + We add a second parameter to + `phosh_util_open_mobile_settings_panel`, allowing + one to send panel options alongside. The panel + name and options can then be packed to remotely + activate settings app accordingly. + * caffeine-quick-setting: Allow to open prefs via mobile settings. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1280 + + [ Danial Behzadi ] + * Update Persian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Martin ] + * Update Slovenian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ AJ Dunevent ] + * device-row: Use 'alias' instead of 'name' for device display. + The 'alias' property is preferred as it contains user-defined names + while falling back to the hardware name if no alias is set. Stays + consistent with other UI elements, similar environments, etc. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1300 + + [ Artur S0 ] + * Update Russian translation + + -- Guido Günther Tue, 10 Feb 2026 18:03:17 +0100 + +phosh (0.52.1) experimental; urgency=medium + + * gvc: Handle port changes on streams. + Use a gvc version that handles an existing stream replacing its + active port. + This works with pipewire 1.5.84 and wireplumber 0.5.13 + + -- Guido Günther Tue, 06 Jan 2026 14:04:46 +0100 + +phosh (0.52.0) experimental; urgency=medium + + * No changes from 0.52~rc1 + + -- Guido Günther Sun, 04 Jan 2026 15:44:21 +0100 + +phosh (0.52~rc1) experimental; urgency=medium + + [ Guido Günther ] + * ruff: Set line length. + This lets + meson test -C _build/ --suite lint --print-errorlogs + pass again. + * build: Bump minimum glib version to 2.80. + Needed for `g_log_writer_default_set_debug_domains` + * tests: Use g_log_writer_default_set_debug_domains directly. + No need to go through a custom implementation + * shell: Track log domains + * debug-control: New DBus interface to control debug settings. + For now we allow to set log levels via DBus. This is similar + to what we do in phoc. + * shell: Export debug control interface. + Allows to set log domains via DBus + * log: Drop custom log handling. + Just use glib's default handler, we can set log domains + via DBus now. + * tests/integration: Use DBus call instead of timeout. + Now that we have a DBus interface that is exported once the + shell is up we can drop the timeout check in favor of a DBus + call thus speeding up the tests. + * ambient: Return early when release got cancelled. + This means `self` is no longer valid so don't try + to set any values on it. + * docs: Document Python dbusmock based integration tests + * docs: Document the screenshot tests + * plugin-prefs-standalone: Add option to show lockscreen plugins. + This allows us to specify the plugin type in all cases. + * plugin-prefs-standalone: Exit gracefully on SIGTERM. + This allows e.g. coverage information to be written out. + * test-take-screenshots: Simplify testing one page + * plugins-prefs: Screenshot upcoming events prefs too + * test-take-screenshot: Wait for toplevels rather than sleeping. + Instead of just guessing whether the app started, make sure it's there + and check when the dialog opened by counting the toplevels. + We also send keyboard shortcuts more than once so we don't have to worry + about them being missed because e.g. the old dialog didn't go away yet + or the window didn't get focus yet. + * test-take-screenshots: Screenshot quick setting prefs too + * test-take-screenshots: Wait for OSD to show + * test-take-screenshots: Introduce context. + About all functions want a loop, a timer, a keyboard and a waiter. + Wrap them in one context. + * test-take-screenshots: Add and remove a keybinding. + We have the proxy anyway so let's make sure we can do this without + throwing any criticals. + * test-take-screenshots: Drop unneeded cast + * tests/integration: Check for running process later. + This improve error reporting as we make sure we read the last output + bits. This is especially useful when phosh fails to start as we'd + otherwise completely swallow the error message. + * tests/integration: Check that we detect Wi-Fi networks. + This exercises more of our Wi-Fi code + * tests/integration: Check that we detect VPNs. + Make sure we can detect VPNs without raising criticals + * dbus: Fix swapped arguments for PhoshIdleDBus. + Otherwise targets depending only on the headers can get it wrong. + See https://gitlab.gnome.org/World/Phosh/phosh/-/jobs/5808582 + Fixes: 6376839df ("build: Separate dbus headers and sources") + * tests/integration: Build udev mocks manually. + This will allow us to tweak them in the tests. + * tests/integration: Test brightness setting + * brightness-manager: Don't raise brightness on idle. + When idle dimming is enabled we never want to increase the brightness. + If current brightness is below target dimming brightness just don't do + anything. + Fixes: 57ee80353 ("brightness-manager: Add Brightness DBus interface") + * tests/integration: Move Wayland socket and runtime dir into a tmpdir. + This improves test isolation + * tests/integration: Allow to use keyfile backend. + We set a separate homedir so we can maintain a custom keyfile and allow + to pass the settings backend when creating the Phosh object. + * tests/integration: Load a custom keyfile + * tests/integration: Load quick settings. + Load quick settings and check for the hotspot one that it got loaded + * brightness-manager: Allow to set the adjustment value. + This makes it simper for other parts of the shell to get and + set the value avoiding to have to get the adjustment first. + * brightness-manager: Split out osd handling. + We'll use it from other places as well + * brightness-manager: Allow to show the osd. + Allow to show the OSD when setting the value + * lockscreen-manager: Add a brightness gesture. + Two finger swipe left to right increases brightness. The new + brightness is calculated as offset to the current brightness + based on the swipe distance. + This means that if something else sets brightness in the meantime it + will be overwritten. If we'd just add the difference between two events + this might be too small to make an actual difference thus increasing the + brightness too slowly. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/814 + * shell: Don't setup primary monitor handler without one. + When running nested the window can be closed leaving us without + a monitor. We shouldn't try to setup any handlers in this case. + * activity: Fix indent + * activity: Use _once function for timeout. + We only run it once. While at that avoid function cast for type + safety. + * activity: Drop explicit notify. + No point in doing so for CONSTRUCT_ONLY properties. + * activity: Minimize set_property. + We don't want to have any logic within the `switch` so simplify + things like we do it in other classes. + * activity: Clarify set_thumbnail. + It is transfer full. Make that more obvious by moving the thumbnail + setting to the top too and adding a doc string. + * overview: Remove unused headers + * overview: Simplify has-activities settings. + Set it in a single place and based on the widgets in the carousel. + No point in counting toplevels. + * app-tracker: Avoid casts + * splash: Modernize. + Avoid function casts, fix indent. + No functional change. + * toplevel-thumbnail: Remove superfluous cast + * toplevel-thumbnail: Modernize property definition + * toplevel-thumbnail: Use correct parent class. + The parent class is PhoshThumbnail not GObject + Fixes: 58d280ba2 ("Introduce PhoshThumbnail and PhoshToplevelThumbnail") + * toplevel-thumbnail: Don't assume specific parent class. + Use `PHOSH_THUMBNAIL_CLASS` to get the class as we do it elsewhere + to set the vfuncs. + * toplevel-thumbnail: Use canonical naming + `self` is the instance of the current type (like everywhere else in + phosh) while we name the parent class type ones consistently + `thumbnail`. The code was a mix of both before with hard to read + casts. + * toplevel-thumbnail: No need to chain properties up to parent class. + GObject does that for us. + * toplevel-thumbnail: Remove unused headers + * thumbnail: Remove superfluous implementations. + They're completely empty + * thumbnail: Unclutter ready handling. + Lots of indirection to set a boolean. Just use the common pattern + where the derived classes set the property on the parent so we save + two vfuncs. + * thumbnail: Remove set_property. + Superfluous for read-only properties. + * battery-manager: New class to track battery and charging. + Needed to move things out of `PhoshBatteryInfo` + Helps: https://gitlab.gnome.org/World/Phosh/meta-phosh/-/issues/25 + * shell: Allow to instantiate battery manager + * battery-info: Name source file properly. + We use a `-` between words. Another reminiscence from the early days + cleaned up. + * battery-info: Use PhoshBatteryManager. + This avoids having multiple upower clients around. + * tests/integration: Allow to save DBus traffic. + This can be helpful to figure out if a message was actually sent + * integration-test: Test battery manager. + Test that the battery manager picks up charging related changes + correctly. + * util: Unify panel launching. + This reduces the code and we also pass on platform data for proper + activation. We'll reuse `phosh_util_activate_action` in other places + too. + * util: Pass platform data when launching panels. + This ensures the app is brought to the foreground if already running. + * overview: Use g_type_ensure() to ensure types + * overview: Install properties. + As we only connect to the notify signal noone noticed. + * overview: Connect notify::has-focus with the others + * overview: Connect activity signal with the others. + Although we strictly don't need overview here we'll do so + at some later point and also check there's a toplevel (which + will also be useful later too). + * overview: Use `g_connect_object` + Reads a bit better here. + * overview: Simplify connecting to toplevel_manager + * overview: Bind app-grid signal via UI template. + Less to worry about + * overview: Connect carousel signal handlers via UI file. + Less to worry about. + * swipe-away-bin: Fix indent + * app-grid: Move declaration to the top. + This is borderline but as we can better indent the + criteria let's do it. + * app-grid: Use type checked cast. + This is no performance critical path + * app-grid: Simplify app search focus handling. + Use `gm_str_is_null_or_empty()` and `phosh_util_toggle_style_class()` + * splash-manager: Use G_PARAM_STATIC_STRINGS + * layout-manager: Avoid gmobile deprecation. + We don't take the effort to handle different radii as with + xdg-cutouts-v1¹ (adjusted for layer surfaces) we'll get the correct radii + without having to worry about screen transforms. + ¹) https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/372 + * torch-info: Always use 100% brightness if torch can't scale. + If the torch can't scale brightnes we want to show 100% indepent + from any min and max brightness set via udev. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1293 + * lockscreen: Ignore gesture when user wants to zoom. + This allows zoom to work in the ticket box again. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1294 + + [ Victor Dargallo ] + * Update Catalan translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Arun Mani J ] + * wifi-manager: Allow to get active connection + * tree: Export few symbols of Wi-Fi manager + * wifi-hotspot-quick-setting: Load `style.css` + Will be used by status-page in subsequent commits. + * wifi-hotspot-quick-setting: Add `PhoshWifiHotspotStatusPage` + * wifi-hotspot-quick-setting: Add status-page. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1259 + * wifi-hotpsot-quick-setting: Uncrustify + * wifi-hotspot-quick-setting: Use class for style + * wifi-hotspot-quick-setting: Show hotspot SSID. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1285 + * wifi-hotspot-quick-setting: Close status-page on deactivation. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1286 + + [ Martin ] + * Update Slovenian translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Achill Gilgenast ] + * ci: Add qrcodegen-dev dependency for Alpine edge + also removed duplicates and sorted the list again + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Rudra Pratap Singh ] + * upcoming-events: Use GListStore to store event lists. + We replace the use of `GPtrArray` with a + `GListStore` as we can then use it in + combination with a `GtkFilterListModel`. + * upcoming-events: Use GtkFilterListModel to filter empty days. + Additionally make `events_box` a `GtkListBox` instead + so as to bind it to the filtered model. + * upcoming-events: Add CSS class for PhoshEventList listbox. + Allows us to keep styling separate for event-list + listbox and events listbox. + + [ Artur S0 ] + * Update Russian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Asier Saratsua Garmendia ] + * Update Basque translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Anders Jonsson ] + * Update Swedish translation + + -- Guido Günther Mon, 29 Dec 2025 18:22:14 +0100 + +phosh (0.51.0) experimental; urgency=medium + + [ Artur S0 ] + * Update Russian translation + + [ Evangelos Ribeiro Tzaras ] + * subprojects: Switch to libcall-ui 0.1.5. + We have a tagged version now. + + [ Antonio Marin ] + * Update Romanian translation + + [ Rudra Pratap Singh ] + * caffeine-quick-setting: Add plugin preferences. + We now have 'caffeine-quick-setting-prefs' + preferences that allow one to add or remove + caffeine intervals. + * caffeine-prefs: Add empty-state for no intervals + * caffeine: Close status page on selecting interval + * caffeine-quick-setting: Activate timer on selecting interval + + [ Martin ] + * Update Slovenian translation + + [ Guido Günther ] + * caffeine-quick-setting: Make prefs dialog follow content size. + Otherwise in bottom sheet mode adding new rows doesn't make the dialog + resize and the user doesn't see the new rows. + * backlight: Allow to force non linear backlight scale. + Add a debug flag that allows us to assume the backlight uses + a non-linear scale. This is meant for situations where the + kernel information is incorrect. + * tests/integration: Use pixman renderer. + Our udev mocks confuse wlroots when trying to get a render node + otherwise + * ci: Use our pytest runner. + This avoids duplicating env vars in multiple places + * tests/integration: Allow to launch Phosh under a wrapper. + Can be used for udev mocks or valgrind + * tests/integration: Mock and test torch and backlight. + * caffeine/prefs: Make empty state compact. + It's for a dialog, not a main window + * torch-manager: Don't return 0 when torch is on. + This makes it simpler for UI elements to display a sane value. 0% with + the LED still on looks confusing and we require + GM_TORCH_MINIMUM_BRIGHTNESS to indicate a still visible LED brightness. + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Robert Eckelmann ] + * torch-manager: Add minimum brightness. + We get it from udev when available and fall back to 5% of maximum + brightness otherwise. + The udev rule to provide minimum brightness overrides via hwdb is + shipped in gmobile. + * settings: Drop minimum brightness. + This is now handeld by torch manager + + [ Sabri Ünal ] + * Update Turkish translation + + -- Guido Günther Sun, 16 Nov 2025 11:27:16 +0100 + +phosh (0.51~rc1) experimental; urgency=medium + + [ Rudra Pratap Singh ] + * upcoming-events: Show appropriate status page in case of no events. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1255 + Fixes: 6614bbd39 ("upcoming-events: Add new setting for skipping non-event days") + * upcoming-events: Reindent UI file + * caffeine: Add libcall-ui as dependency + * caffeine: Refactor icon names into defines. + No functional change. + * caffeine: Allow to select caffeine duration. + With this, we add support for custom intervals + in caffeine. It will decide how long caffeine + will stay on for. Intervals can be added/selected + individually via gsettings keys. + + [ Guido Günther ] + * layout-manager: Don't forget to init corner shift. + This ensures we have the correct default in case of no rounded corners. + * top-panel: Shift gear icon according to corner shift + * top-panel: Drop `name` property. + Fixes the + (phosh:1106322): Gtk-WARNING **: 17:26:18.095: GtkBox does not have a child property called name + warning on startup. + Fixes: b567c6ed3 ("top-panel: Add gear button") + * doc: Use apt instead of apt-get. Let's be modern here. + * screenshot: Update screenshot. Let's be at least as recent as Wikipedia. + * ci: Update shared scripts to 7aa7dc1 + * treewide: Update my copyright assignment. Let's use the fiduciary agreement. + * ambient: Avoid cast. This gives us more type safety + * ambient: Rename settings to schema. + The later is more wide spread + * udev: Handle fake-builtin debug flag. If set assume that the found + displays are built-in. This allows to test backlight handling in + nested sessions. + * auto-brightness: Add interface for turning ambient into display brightness + * auto-brightness: Add bucket implementation. + This implementation uses buckets to determine the + brightness level. + * shell: Create ambient manager earlier. + We'll need it for the brightness manager which gets created + with the panels. We also add a getter. + * ambient: Track whether display brightness should be adjusted. + If so emit the ambient brightness values so other parts of the shell can + consume them. + * ambient: Track automatic high-contrast. + Avoids dconf lookups and is a bit easier to read + * brightness-manager: Track ambient brightness changes. + When enabled we adjust the backlight brightness according + to the auto-brightness trackers output. + * data: Add auto-brightness-symbolic icon. + I wanted to draw my own but then accidentally found one by + Jakub Steiner in org.gnome.design.IconLibrary. + * brightness-manager: Expose auto-brightness-enabled. + This allows users of the brightness manager to use it + without having to care about e.g. the ambient manager. + * settings: Use different icon when autobrightness is in use + * emergency-calls: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * emergency-calls-manager: Use correct types for async callbacks. + This gives us more type safety + * build: Allow to codegen object manager too + * calls: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * calls-manager: Use correct types for async callbacks. + This exposed a real bug where the data argument used the + wrong type (but was unused). + * calls: Use correct types for async callbacks. + Gives us more type safety + * location-manager: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * location-manager: Use correct types for async callbacks. + Gives us more type safety + * session-manager: Use default DBus prefix for `ClientPrivate` + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * session-manager: Use correct types for async callbacks. + Gives us more type safety. + * session-presence: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * osk-manager: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * osk-manager: Use correct types for async callbacks. + This gives us more type safety. + * rfkill-manager: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * rfkill-manager: Use correct types for async callbacks. + This gives us more type safety. + * mpris: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + * mpris-manager: Use correct types for async callbacks. + This gives us more type safety. + * mpris-player: Use correct types for async callbacks. + This gives us more type safety. + * background-manager: Avoid criticals when monitor goes away first. + We emit a critical when the monitor goes away before the layer-surface. + However that doesn't really matter to us so make it a debug message. + * audio-devices: Ignore role loopbacks. + Users shouldn't switch to the media role loopback devices + * location-manager: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + In this case everything get a lot shorter as the `interface_prefix` + didn't match and we thus had the full `org_freedesktop` in all the + names. + * notify-manager: Use default DBus prefix. + This allows us to move to `dbus_client_protos` so our generated header + gets added to `generated_dbus_headers` instead of + `generated_dbus_sources`. + In this case everything get a lot shorter as the `interface_prefix` + didn't match and we thus had the full `org_freedesktop` in all the + names. + * build: Separate dbus headers and sources. + The idle monitor needs an object manager. Moving it into + dbus_server_protos would create one with a `PhoshDBus` + prefix clashing with the one from `org.gnome.Calls.Call` + so keep it separate for now but split headers and + sources already. (Closes: #1276) + * ambient: Improve readability. + Get the proxy ones instead of producing overly long cast lines + * ambient: Guard against `NULL` unit + g_ascii_strcasecmp() doesn't handle that gracefully and it happens + e.g. on laptops after resume. + * ambient: Handle settings changes in a one place. + Otherwise it is harder to determine if we should claim the sensor + base on auto-hc and auto-brightness settings. + * ambient: Reclaim sensor on unblank. + The current code didn't necessarily do this. + * ambient: Simplify claim / unclaim callbacks. + Avoid the casts, bumping readability and making claim and unclaim more + alike + * ambient: Release sensor more thorougly. + Otherwise we don't reclaim. In order to avoid overlapping calls + of claim and release clearing the flag we make it a counter instead + of a plain boolean. + * ambient: Separate HighContrast handling from ambient. + This makes is easier to invoke it from multiple places. We e.g. + forgot to stop sampling when the sensor went away. + * auto-brightness-bucket: Only notify when index changes. + No need to emit the signal if it didn't and thus brightness + wouldn't change. + * backlight: Allow to get brightness levels. + Will be used in the next commit + * brightness-manager: Use brightness transition. + This makes brightness changes less noticeable + * ci: Drop gstyle check. + It fails on contrib code that we don't want to modify. + * media-player: Move media loading to separate function + * media-player: Don't reload cover art if the URL didn't change. + Players like kasts trigger a whole lot of meta data updates grinding + devices like the Librem 5 to halt when loading the icon. + * brightness-manager: End transition immediately when disabled. + When the user toggles automatic brightness off we want to end the + transition right away. + Fixes: f50546254 ("brightness-manager: Track ambient brightness changes") + * brightness-manager: Trigger auto brightness transition when enabled. + Always trigger an auto-brightness transition when enabled. In dark + environments the ambient sensor might not put out a new value but + we still want to transition right away when the user enables it. + Fixes: f50546254 ("brightness-manager: Track ambient brightness changes") + * brightness-manager: Use relative brightness for keybindings too. + Instead of calculating based on the brightness range use relative + brightness and the number of levels the hardware supports. + * brightness-manager: Use adjustment range [0.0, 1.0] + This is what we use for the backlights and there's nothing + gained when we convert to percentage. + * brightness-manager: Allow to get auto-brightness-enabled + * backlight: Track scale. + This allows to track whether the backlight exposes a linear + or exponential scale. + * backlight: Use anonymous struct. + Better groups the variables + * backlight: Separate backlight level from exposed brightness. + When we want the brightness curve to be non-linear be need two + values: those exposed to the user facing side and those values + actually set in the hardware (called `level`s). + We do the split here but use a linear mapping only. This will + change in the next commits. + * backlight: Use logarithmic scale. + If the backlight type is unknown or linear we use a logarithmic + scale to match the human eye's brightness perception, see + https://en.wikipedia.org/wiki/Weber%E2%80%93Fechner_law + This lets the brightness slider in settings appear linear over the + whole scale instead of doing almost nothing in the upper half. + * brightness-manager: Make slider an offset when auto-brightness is on. + With autobrightness enabled we want the brightness slider to be an + offset to the auto brightness target rather than the actual brightness. + * brightness-manager: Update autobrightness on monitor changes + * brightness-settings: Remove origin when auto-brightness is enabled. + As this is an offset now we don't want to draw the origin part + Helps: https://gitlab.gnome.org/guidog/meta-phosh/-/issues/19 + * brightness-settings: Use icon. + This makes it consistent with the audio settings + * brightness-manager: Provide a suitable icon based on current state. + Similar to other managers provide an icon-name based on the current + state (auto-brightness enabled or not). We need that for the OSD and can + then reuse it in other parts of the shell. + * brightness-settings: Use brightness-manager's icon-name property + * ci: Update to forky + * libcall-ui: Update to latest 0.1.x branch. + Avoids clang build failures + * plugins: Avoid warning when copying symlinks. + Otherwise we might end up copying the symlink with newer meson. We + need to bump the minimum meson version for that. + Fixes: fda76c376 ("mobile-data: Install icon to user's filesystem") + * doap: Drop Zander. Not active since several years. Thanks Zander for + your work! + * doap: Add Arun + * wifi-manager: Fix log domain + * ci: Use headless backend for integration tests. + No need to have the tests guess + * tests/integration: Specify Wayland socket. + That is more reliable than parsing it from the output + * tests/integration: Don't always close stdout. + This allows us to parse phosh's output + * tests/integration: Ease output checking. + Allow tests to access phosh's output more easily. + They can specify if the check should fail when the searched for string + is already present. + * tests/integration: Allow to tweak phosh's environment. + This allows to e.g. set logging levels without polluting `os.environ`. + * tests/integration: Simplify running pytests locally. + Setting the env vars can be cumbersome so make it as simple as + * tests/integration: Make is simple to save test logs + * tests: Lint Phosh helper too + * ci: Use pixman renderer for integration tests. Avoids criticals. + * tests/integration: Disable XWayland. + We don't need it in CI + * tests/integration: Add initial system service test. + We interact with quiet a bit of system services but don't exercise this + in the tests so far. Make sure we at least look for a modem. + * tests/integration: Allow phoc to exit cleanly. + We handle `SIGTERM` gracefully nowadays so allow for that too. + * ci: Set default build opts. No need to set them on jobs individually + * ci: Minimize script sections. We move the variable setting to + `variables:` + * ci: Use phoc 0.51~rc1 + * ci: Collect junit reports from tests run by meson as well + + [ Martin ] + * Update Slovenian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Artur S0 ] + * Update Russian translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Arun Mani J ] + * simple-custom-quick-setting: Simplify files. + Remove unnecessary headers and uses 2 lines separation between + functions. + * caffeine-quick-setting: Simplify files + * dark-mode-quick-setting: Simplify files + * mobile-data-quick-setting: Simplify files + * night-light-quick-setting: Simplify files + * pomodoro-quick-setting: Simplify files + * scaling-quick-setting: Simplify files + * wifi-hotspot-quick-setting: Simplify files + * ci: Ignore deleted files in `check-ui` + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1274 + * tree: Add `PhoshBrightnessSettings` + * settings: Use `PhoshBrightnessSettings` + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1272 + * settings: Clean up unused headers + + [ Gotam Gorabh ] + * plugins: Add location quick setting. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/783 + * build: Define 'phosh_plugin_icon_dir' + * caffeine: Install icon to user's filesystem + * dark-mode: Install icon to user's filesystem + * night-light: Install icon to user's filesystem + * pomodoro: Install icon to user's filesystem + * scaling: Install icon to user's filesystem + * wifi-hotspot: Install icon to user's filesystem + * mobile-data: Install icon to user's filesystem + * location: Install icon to user's filesystem + * debian: Install plugin's icon + * ci: Use common gchar check + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + -- Guido Günther Sun, 09 Nov 2025 17:25:42 +0100 + +phosh (0.50.0) experimental; urgency=medium + + [ Guido Günther ] + * shell: Move osd window here. + This allows us to use the osd window not only via DBus and + it doesn't warrant its own manager object. + * backlight-manager: New object to manage backlight brightness. + This manages keybindings, OSD, etc as we don't want that put into + a brightness slider. We can later on use it to manage the brightness + of different backlights (instead of just the primary one). + * shell: Instantiate backlight manager + * brightness-manager: Make keybindings optional. + The corresponding schema is not very wide spread yet so don't error + out if the schema isn't recent. + * simple-custom-quick-setting: Hide in settings by default. + We don't want to show demo plugins there + * calendar: Hide in settings by default. + We don't want to show demo plugins there + * ci: Trigger libphosh-rs pipeline. + Adjust to the new repo location + * monitor-manager: Move mode building to separate function. + We'll make use of this in the next commit + * monitor-manager: Only skip unnamed mode if not the current one. + This helps e.g. when running nested as those configs only have + a single unnamed mode. + * monitor-manager: Add default mode when there's no mode list + * data: Depend on OSK target + gnome-session >= 49 no longer handles `RequiredComponents` + in session files. We hence need start the OSK via systemd. + Do so by providing a target unit. The OSK implementations then + merely need to fill in the service unit. + * ambient: Reset theme when turning of auto-hc. + We want to make sure the users has back its default theme + when they disable automatic high contrast. + * session: Start the OSK later. + We want the layer ordering to happen after the shell is up. + * brightness: Remove unused variables. + Fixes: aa0066c5a ("settings/brightness: Use built in brightness handling") + * backlight: Add and use methods to get and set relative brightness. + No need to make the calculations in multiple places + * shell: Allow to get brightness manager. + We want to get it in `PhoshSettings` + * brightness-manager: Maintain an adjustment. + Maintain an adjustment of brightness percentage as model. That frees + other parts of the shell from doing so. + * settings: Use brightness manager's adjustment. + That way we don't need to worry about any monitor changes + and while at that can get rid of the somewhat arcane code + (the brightness slider was one of our first features :)). + * brightness-manager: Add Brightness DBus interface. + The DBus API XML is from gnome-shell as of: + e54aa588c ("slider: Use the sprite of the touch event, not the pointer") + * brightness-manager: Move keybindings schema check to end of init. + Otherwise we exit early with older schema disabling brightness + handling completely. + This allows us to run with schemas < 49.0. + * settings: Only show brightness slider when useful. + For now we show it when we have brightness control. We might + also hide it later on when automatic brightness is enabled. + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + -- Guido Günther Sun, 05 Oct 2025 17:02:58 +0200 + +phosh (0.50~rc1) experimental; urgency=medium + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Philipp Kiemle ] + * Update German translation + + [ Guido Günther ] + * treewide: Prefer char over gchar. + * tests: Use g_timout_add_once. + Our on_waited never acturally returned `FALSE` to stop the + timeout. + * wifi-network-row: Use correct type for transform func + * wifi-status-page: Use correct function signature. + * build: Fix indent. + We can use a list so that the automatic formatter doesn't get confused. + Fixes: b2fcd0dcf ("build: Format meson files") + * doc: Update issue template. + Some information is nowadays easier to maintain, debugging + is better documented. + * quick-settings: Enable wifi-hotspot and mobile data by default. + * settings: Disable launching mobile panels on lockscreen too. + Fixes: af37b2c74 ("settings: Add action to open mobile settings") + * top-panel: Move launching settings here. + This allows us to use it from all places in the top panel + * icons: Add settings icon. Taken from org.gnome.design.IconLibrary. + * top-panel: Add gear button. This gives us easy access to the top panel + configuration + * treewide: Use stevia. phosh-osk-stub is no more. + * location-manager: Avoid cast. This gives us more type safety + * location-manager: Use cancellable more. Cancel these calls too on shutdown + * proximity-manager: Use cancellable. Fixes a test suite crash + * location-manager: Dispose connection on shutdown. + The object returned by `g_bus_get_finish()` is a singleton + but we need to unref it nevertheless. + * wwan-mm: Avoid cast. + This gives us more type safety + * wwan-mm: Avoid cast. + This gives us more type safety + * wwan-mm: Dispose connection on shutdown + * monitor-manager: Avoid cast. + This gives us more type safety + * tests: Make sure gvfs doesn't spawn in tests. + We don't want it to take any DBus connection that could block + GTestDBus shutdown. + * util: Improve phosh_dbus_service_error_warn() + We usually want to special case "cancelled" and + "doesn't exist" in the tests. + * feedback-manager: Ignore service unknown errors. + These can happen in tests + * tests: Set XDG_CURRENT_DESKTOP. + This ensures we pick up Phosh's gsettings defaults + * ci: Udpate image. + Let's update to Trixie before we switch to Forky soon. + * ci: Shuffle dockerfile a bit. + Update everything first rather than in between. The + removal of the comment ensures we actuall get the build-deps + installed again thus speeing up the build. + * keyboard-events: Check for error. + It is an initable so lets use it + * util: Add helper to construct keybindings. + This happens in different places of the shell and is repetitive + * top-panel: Use keybindings builder + * home: Use keybindings builder + * home: Don't forget to add the super key entries. + We want to have them in the keybinding names as well so they + get removed on dispose. + * run-command-manager: Use keybindings builder + * screenshot-manager: Use keybindings builder + * gitignore: Ignore wraplock + * monitor-manager: Drop change_backlight helper. + No longer invoked by g-s-d + * keyboard-events: Drop protocol version check. + We require a recent enough version since ages + * shell: Init settings early. + We want to do as little as possible in constructed + * shell: Move toplevel manager to init. + We want to do as little as possible in constructed + * shell: Set resource path in init. + We want to do as little as possible in constructed + * udev-manager: New class to handle udev interaction. + We need a client for torch and backlight. Track it in a common class + and add the helpers we'll need for backlight handling. + * udev-manager: Get us a login session proxy. + While not strictly udev related torch and backlight both need it + and there's no point in introducing yet another class. + We might want to turn that into a more generic "ShellBackend" class that + could e.g. also track gsettings. + * shell: Instantiate udev manager. + It's a singleton but lets give it clear life cycle + * packaging: Version g-s-d dependency. + We want one entity to control brightness + * backlight-sysfs: New class to manage sysfs based backlights. + There might be others later, e.g. ddc and thus introduce a + `PhocBacklight` base class. This is inspired by what g-s-d 48 did and + what mutter 49 does now so we can continue to interact with g-s-d power. + * monitor: Create backlight. + The backlight is attached to the monitor so we can look it up + for brightness handling. + * settings/brightness: Use built in brightness handling. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1261 + * udev-manager: Support torch subsystem too. + Use that from the torch-manager + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Anna (cybertailor) Vyalkova ] + * tests: Don't assume build directory is inside source directory. + Fixes: 4c58fc4fb ("searchd: Add deamon to gather search results") + + [ Evangelos Ribeiro Tzaras ] + * meson: Require rst2man for building manpages. + The build option description already specifies that rst2man is + required, but it's not codified in the build files. + + [ Nathan Follens ] + * Update Dutch translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Kristjan SCHMIDT ] + * Update Esperanto translation + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + -- Guido Günther Mon, 29 Sep 2025 18:05:54 +0200 + +phosh (0.49.0) experimental; urgency=medium + + [ Guido Günther ] + * search: Link statically. + Otherwise searchd would require an additional shared + library. + * search: Don't install library. + We don't want to ship another shared library + * searchd: Honor meson option. + Just having an option isn't enough, we also need + to take it into account. + Fixes: 4c58fc4fb ("searchd: Add deamon to gather search results") + * media-player: Load file icon as pixbuf. + This will allow us to tweak it if needed. It also + makes the read async. + * media-player: Add setter for image + * media-player: Better handle non square icons. + The thumbnail provided by Firefox for e.g. youtube isn't + square. Gtk-3 top aligns the thumbnail making it look odd, + center the thumbnail to make it look better. + * media-player: Round corners of album art. + We round corners via cairo in + https://gitlab.gnome.org/World/Phosh/phoc/-/merge_requests/704 + and can thus move the logic over here too. This can be dropped + when we switch to Gtk-4 as it can handle that for us. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/697 + * treewide: Use `-` instead of `_` for swapped-object-signal + * abi: Allow to access phosh_shell_get_type() + Plugins should be able to perform type checks. + * pomodoro-quick-setting: Allow to start timer on screen unlock. + Otherwise it is easy to forget to turn it on. + * pomodoro-quick-setting: Start on login. + For the user this isn't easily distinguishable from a regular unlock. + Since the top-panel can be recreated we use a global variable. + * phosh-pomodoro-quick-setting-prefs: Shorten key names. + They're only used in this compilation unit + * phosh-pomodoro-quick-setting-prefs: Allow to set 'start-on-unlock' + * upcoming-events: Use custom icons. + We indicate empty vs non-empty rows rather than suggesting + a sorting order. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1245 + * libphosh: Add explicit dependency on generated headers. + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Martin ] + * Update Slovenian translation + + [ Achill Gilgenast ] + * phosh-keypad: Use alpha when focused. + Follow-Up from https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1250 + * phosh-search-bar: Use alpha background color. + Doesn't look so out of place in the overview + + [ Artur S0 ] + * Update Russian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Gotam Gorabh ] + * searchd: Fix crash by passing GStrv instead of GVariant to subsearch. + Fixes: 4c58fc4f ("searchd: Add deamon to gather search results") + + -- Guido Günther Fri, 15 Aug 2025 10:12:52 +0200 + +phosh (0.49~rc1) experimental; urgency=medium + + [ Arun Mani J ] + * app-grid: Set max-width to entries. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/703 + * ci: Add `xvfb` package for `check-ui` job + * tree: Bump `upower-glib` version + * battery-info: Use asynchronous API of `upower` + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/290 + + [ hustlerone ] + * plugins: Fix compilation on NixOS. + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + [ Guido Günther ] + * mode-manager: Support embedded chassis type. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1242 + * plugins/media-players: Disable prefs plugin. + There's no prefs atm so remove it from the desktop file as + otherwise mobile settings doesn't set the gear icon insensitive. + * ci: Update shared scripts to 7e4c544 + * tests: Use subdir_done. + Avoids an indent level when using meson format + * build: Format meson files. + Format using `meson format -e -i -r` to make contributing + easier with fewer review rounds. + * ci: Check format of meson files` + They're only validated when they change + * data/portals: Use phrosh portal. + This way we still fall back to gtk in cases where distros can't ship the + new portal yet. We'll prefer phrosh over phosh once pfs and wallpaper moved + over too so we then only use the phosh portal for settings. + * wwan: Require cell broadcast support in ModemManager. + It's in a release so no need to conditionalize + * system-modal: Modernize property definition. + Match coding style and fix some indent while at that. + * cell-broadcast-prompt: Limit messages to reasonable size. + Otherwise the dialog might drop off screen. Since we have + a store the user can look up overly long messages there. + * cell-broadcast-prompt: Close dialog on swipe. + Fixes: b281cc5d3 ("cell-broadcast-prompt: New dialog for cell broadcast message") + * cell-broadcast-prompt: No need to valign the button + * cell-broadcast-prompt: Remove the superfluous box + * cell-broadcast-prompt: Strip whitespace. + Some messages (e.g. Vodafone test messages) have excessive + whitespace at the end of the message, strip that. + + [ Zander Brown ] + * searchd: Add deamon to gather search results + org.gnome.ShellSearchProvider2.xml taken from gnome-shell as of + 9e5dfa22211677cf71791925a68688a3b3284c7e + Co-authored-by: Gotam Gorabh + + [ Evangelos Ribeiro Tzaras ] + * mock-mm-nm: Execute with intended interpreter. + Run through `env` so we pick up the correct interpreter from `$PATH` + when e.g. using venv. + + [ Rudra Pratap Singh ] + * upcoming-events: Extend API to get number of events for a list + * upcoming-events: Add icons for expanding/shrinking event list + * upcoming-events: Add new setting for skipping non-event days + * upcoming-events: Add button to skip non-event days. + This will allow users to include/exclude days + without events. + * upcoming-events: Show proper date if number of days exceeds 7 + + [ Martin ] + * Update Slovenian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Artur S0 ] + * Update Russian translation + + [ Danial Behzadi ] + * Update Persian translation + + -- Guido Günther Fri, 08 Aug 2025 19:48:42 +0200 + +phosh (0.48.0) experimental; urgency=medium + + [ Anders Jonsson ] + * Update Swedish translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Álvaro Burns ] + * Update Brazilian Portuguese translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Artur S0 ] + * Update Russian translation + + -- Guido Günther Mon, 30 Jun 2025 08:45:35 +0200 + +phosh (0.48~rc1) experimental; urgency=medium + + [ Guido Günther ] + * auth: Drop priv. + It's unclear why we added that in the first place. + * auth: Use automatic cleanup + * auth: Name authentication token properly. + We had a mixture of `number` and `pin` although we allow for for non-PIN + passwords since a long time. + Also name the appdata_ptr properly so it is easier to follow how this is + passed through to the callback. + * auth: Copy auth token. + Otherwise we're dependent on the life cycle of the GtkEntry that + provides the data. + * auth: Perform account validity checks. + We should perform the account validity checks via pam_acct_mgmt too. + * lockscreen: Don't leak lockscreen on error. + This is unlikely to happen (it's not the same as auth failing) but let's + better fix it. + * media-player: Don't forget to init cancellable. + Otherwise cancelling will have no effect and we might end up accessing + free'd memory. + Fixes 2ca148785 ("media-player: Use MPRIS manager") + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1234 + * media-player: Init fetch icon cancellable. + Fixes f287f5deb ("media-player: Fetch cover art via http") + * packaging: Add valac. + Needed to build the vapi file + * build: Allow to build vapi files + * ci: Build vapi when building bindings lib + * stylesheet: Use accent color for list box rows too. + Fixes 317ef8e89 ("style-manager: Support accent colours") + * lockscreen: Always queue a draw when transition ends. + Otherwise we might not only see an outdated clock (which the + current workaround adressed) but also outdated plugin state. + * mpris-manager: Check for valid player sooner. + This avoids printing debug messages for bus owners that don't + implement the mpris interface. + * mpris-manager: Track all players. + TODO: drop players when gone + * mpris-manager: Allow to fetch known players list model + * media-player: Make it a derivable type. + No functional change + * default-media-player: New widget that will track the default mpris player. + This widget will behave the same as media-player did: track the newest + mpris proxy on the bus. + In this commit we just add the empty class so we can move existing code + over to this widget without changing functionality. + * media-player: Move media player setting to public method. + This will allow us to track arbitrary mpris DBus proxies + * default-media-player: Move default player tracking here. + Free the media player base class from tracking the default player. + This allows us to use any player with that class. + * shell: Make mpris manager accessible to plugins + * media-players: New lockscreen plugin to track all mpris players. + One often has multiple mpris capable players running (e.g. podcasts + and music player). The lock screen plugins allows to switch between + them without effort. + * tests: Name unit test variable accordingly + * wall-clock: No need to get `priv` twice + * wall-clock: Avoid libgnome-desktop in public header. + Since the API is not stable lets use gdesktop-enums instead + as we only need the GDesktopClockFormat enum. + * wall-clock: Add function to strip am/pm from clock strings + * lockscreen: Use phosh_wall_clock_strip_am_pm + * wall-clock: Use a regex match to get the stripped clock + `AM` and `PM` can be localized strings so look for the number part + when we have any whitespace in the clock format. Otherwise we assume + 24h clock. + Hours and minutes can be separated by unicode characters so we can't + just use `:` as separator. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1237 + * portals: Disable currently unimplemented ones. + Otherwise we'll spawn other, possibly non-working implementations. + * ci: Drop gcovr workaround. + This has long been fixed in Debian. + This reverts commit 1edb73a65e854f8f89d5d6802bcd3cf45c04aed5. + * ci: Configure locales in images rather than during the ci run. + This cuts down on ci run times + * ci: Use updated images + * ci: Require `Signed-off-by:` + We do this in the other projects since some time + * tests: Instantiate metainfo cache once. + Otherwise it's not getting destroyed and might do funky things + on shutdown. (Helps: #1240) + * cell-broadcast: Enable by default. + MM bits are in released versions and there's support in mobile-settings + to configure channels so we can flip it on. + + [ fossdd ] + * data: Update list of adaptive apps. + Loupe and Showtime already merged the patches to list their form + factors, while the MR in gnome-calendar is still open. + * data: Fix typo in override gschema + + [ Arun Mani J ] + * activity: Avoid using `` + * app-grid-base-button: Avoid using `` + * quick-setting: Avoid using `` + * app-grid: Avoid using `` + * emergency-menu: Avoid using `` + * home: Avoid using `` + * media-player: Avoid using `` + * gtk-mount-prompt: Avoid using `` + * network-auth-prompt: Avoid using `` + * notification-frame: Avoid using `` + * polkit-auth-prompt: Avoid using `` + * system-modal-dialog: Avoid using `` + * check-deprecated-ui-props: Search from source root. + When run by `meson test`, the current working directory is build root, + so some UI files (like of plugins) are not searched. + * check-deprecated-ui-props: Add check for deprecated packing attributes + * ticket-box: Use `{h,v}expand` instead of `expand` + * quick-settings-box: Support RTL direction. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1154 + * ci: Allow translation files of GTK 3 in screenshot container + * ci: Move `SID_PKGS` to `debian.Dockerfile` + * ci: Update to v0.0.2025-06-06 image + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Martin ] + * Update Slovenian translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Daniel Rusek ] + * Update Czech translation + + -- Guido Günther Sat, 21 Jun 2025 17:46:50 +0200 + +phosh (0.47.0) experimental; urgency=medium + + [ Antonio Marin ] + * Update Romanian translation + + -- Guido Günther Sun, 18 May 2025 09:35:38 +0200 + +phosh (0.47~rc1) experimental; urgency=medium + + [ Guido Günther ] + * abi: Remove symbols duplicated between plugins and shared library. + Users of the shared lib and plugins should be able to access the quick + setting and its status pages. Since the shared lib exports the plugins + symbols there's no point in having different subsets listed in both. + * docs: Mention the symbols files. Give a hint where to add new symbols. + * splash: Use larger icon again + https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/commit/a1c8a0f61e27e1bcda03412dec772b7a1ed5a795 + removed the spinner icon, hence GTK3 added a one in + https://gitlab.gnome.org/GNOME/gtk/-/merge_requests/8354 + but picks the 16x16 one which is a bit too small. + The 16x16 icon is fine for the other places we use a spinner, like + status pages. + This can go away with the switch to AdwSpinner. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1222 + * build: Depend on libfeedback with sound file support + * notify-feedback: Handle cellbroadcast events + * app-grid: Fix missing icon + emblem-ok-symbolic got remove in the 48 adwaita icon theme: + See https://gitlab.gnome.org/GNOME/adwaita-icon-theme/-/commit/a00466f12bd5419d6f22c1097a16fd670431c9d4 + * data: Update list of adaptive apps. + TextEditor announces this via the desktop file now while calendar + doesn't + * notify-feedback: Fix inverted logic for cell broadcasts. + Also guard against `NULL` category + Fixes af35ea749 ("notify-feedback: Handle cellbroadcast events") + * notify-feedback: Check basic events. + The logic is going more complex and we had subtle errors so lets + add some checks. + * notify-feedback: Use "notification-missed-generic" + This is available since feedbackd 0.6.0 and we require 0.7.0 nowadays. + * notify-feedback: Check for NULL upfront. + Allows us to use `g_str_equal`. + * tests: Add options to get core dump on asserts under ASAN. + Without these ASAN just aborts without a coredump. As it is not useful + in CI just leave as a comment for now. + * app-list-model: Fix type of debounce it + g_timeout_add_* return `guint` not `gulong`. + * app-list-model: Disconnect debounce on finalize. + Otherwise we might have running timers when the application quits + * app-list-model: Expand test. + Assert that we finalize properly and also check the state + when app infos got loaded. + * app-list-model: Track StartupWMClass as well. + Needed to betterb match toplevels to app-ids + * util: Simplify return value + * util: Match on StartupWMClass as well. + This will allow more cases to work without guessing and we can drop + some heuristics + * util: Drop applications that have a proper StartupWMClass. + No need to track them manually anymore, they all use `StartupWMClass`. + This makes the expected case the first one so things get a little + faster too. + * util: Drop match by last component. + These apps should have a proper StartupWMClass. + * lockscreen: Check for info widgets upfront. + This ensures we have a small clock e.g. when locking the screen with an + ongoing call. This will also be needed with the following commits as the + media widget will be visible initially and hence there won't be a + changed signal. + We hence move from counting invocations to checking the individual + widgets. + * media-player: Don't focus on click. + We want to avoid the cursor frame when touching the widget as it has no + use. + * lockscreen: Set initial focus widget. + Otherwise we end up with the focus on the first focusable widget which + is the media player's detail button and that doesn't look nice. + By setting it to `submit` we ensure that a signal tab gets it to the + first info widget. + * mpris-manager: Manager object that tracks MPRIS players. + This will be used by the media-player widget so we don't have multiple + places tracking the DBus name owner changes. + * media-player: Use MPRIS manager. + This avoids the media widget appearing on the lock screen slightly after + lock as we don't have to wait for the DBus calls anymore. + * monitor: Don't track make and model + wlroots will stop sending these in + https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5045 + and we don't use them anyway. + * treewide: Add missing G_PARAM_STATIC_STRINGS. + Avoids a bunch of allocations on startup. + * status-pages: Drop signal_emit_by_name argument. + The `done` signal does not have any parameters. For consistency with + other `g_signal_emit_by_name` in our codebase we pass `NULL` as the + return value location (although it could be omitted). + * notify-manager: Move show-banners to header. + We want to use it in a status page + * data: Add chat icons from GNOME's icon library + * util: Add helper to open mobile settings panels. + We add a separate function as the parameters are slightly different. + * settings: Add action to open mobile settings. + We add a separate action as that makes it more obvious what we're + opening. + * feedback-status-page: Add new status page for feedback settings. + This will allow to enable/disable do-not-disturb mode + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1183 + * quick-settings: Add feedback status page + * feedback-status-page: Keep feedback in sync with dnd. + We want to switch to silent when dnd is enabled and back to the old + profile when disabled. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1095 + * polkit-auth-prompt: Don't set icon pass by polkit. + Applications usually pass in the app name which we don't have an icon + for so we usually show the missing icon which is not great. + Ideally we'd show the user avatar there along with more user details. + * notifcation-banner: Unref animation on dispose. + This ensures we run it on gtk_widget_destroy already. + * shell: Clean up banner correctly. + The banner is a toplevel so we want to use gtk_widget_destroy on it. We + connect to the `destroy` signal so in case the layer surface invokes + `gtk_widget_destroy` we don't do it twice. + This avoids a sporadic segfault in the tests when the banner was still + being animated and also avoids a weak pointer where it is not needed. + Fixes 5d4f590b9 ("notification-banner: Add slide-up animation to banner") + (which wasn't the cause but exposed the bug). + Fixes c5556891f ("notifications: maintain a message list") + * osd-window: Don't let it take all the vertical space. + Similar in spirit to + bd8c8c13c ("notifiction-banner: Use valign center") + so we don't cover the space below the OSD with a transparent window + swallowing all input. + * osd-window: Ensure we propagate the width. + As we don't want to allocate the full width anchor to top and set the + width on map. + * wifi-manager: Slightly improve debug messages. + Use Wi-Fi spelling consistently, make messages a bit more expressive + * wifi-network-row: Don't set active indicator visible. + We sync it with a property in `init` so save that extra setup step. + * wifi-network: Don't unconditionally overwrite active access point. + We mustn't overwrite the active access point with an inactive one. + This fixes the missing tick mark in the UI when a network has more than + one access point. + The network is still marked as inactive via `reset_active_wifi_network` + -> `phosh_wifi_network_update_active`. + Fixes 7b83747fc ("wifi-network: Create PhoshWifiNetwork class") + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1180 + * mobile-data-quick-setting: Make sure the modem is unlocked. + If the modem's SIM is still locked there's no point in allowing to + toggle mobile data. + Fixes 20ac30db5 ("plugins: Add mobile data toggle") + * app-list-model: Use app_info + `l->data` is harder to parse and we assigned to `app_info` just above + Fixup indent while at that. + * app-list-model: Ignore folders for startup wm class tracking. + Folder don't have a wm class and otherwise raise a critical + Fixes 5b9a575c4 ("app-list-model: Track StartupWMClass as well") + + [ fossdd ] + * calendar-server: Use a non-declared variable name for timezone + * call-notification: Cast PhoshCallState explicitly to CuiCallState + * testlib: Declare on_phoc_startup_timeout as noreturn + * system-prompt: Avoid declaration of unused variable + * ci: Add debian-trixie-clang CI. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1218 + + [ Baxrom Raxmatov ] + * Add Uzbek (Latin) translation + + [ Arun Mani J ] + * status-icon: Add pixel-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + Deprecates `icon-size`. + * tree: Use pixel-size in status-icon + `icon-size` is deprecated in status-icon. + * quick-setting: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * quick-settings-box-standalone: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + * app-grid-button: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * app-grid-folder-button: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * app-grid: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * activity: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + * lockscreen: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + * app-auth-prompt: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + * gtk-mount-prompt: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * polkit-auth-prompt: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. For the functions that need an icon-size just for namesake, + let's use `-1` so we can ensure that we have replaced its functionality + with pixel-size. + * ticket-box: Use pixel-size instead of icon-size + `GtkIconSize` values are semantic in GTK 4 and no longer contain the + various values we have in GTK 3. If we need to enforce a specific + pixel-size, we need to use the `pixel-size` property of image. So let's + move to it. + * app-grid: Set pixel-size for images. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1224 + Fixes: 2ed70a190 ("app-grid: Use pixel-size instead of icon-size") + * subprojects: Update libfeedback revision. + Fixes: 5bed6427dde4f3c8ca0cf3a4b8f5e2a7ff34b861 + * quick-setting: Use checked instead of active state + `:checked` is the proper state to describe the "activeness". `:active` + denotes whether the button is being activated by user, that is, if it is + being pressed down. + * audio-device-row: Use `margin-*` instead of `margin` + * media-player: Use `margin-*` instead of `margin` + * osd-window: Use `margin-*` instead of `margin` + * power-menu: Use `margin-*` instead of `margin` + * top-panel: Use `margin-*` instead of `margin` + * wifi-status-page: Use `margin-*` instead of `margin` + * calendar: Use `margin-*` instead of `margin` + Also add the XML header for easier file identification. + * emergency-info: Use `margin-*` instead of `margin` + * launcher-box: Use `margin-*` instead of `margin` + * ticket-box: Use `margin-*` instead of `margin` + * upcoming-events: Use `margin-*` instead of `margin` + * notify-blocks: Use `margin-*` instead of `margin` + * check-deprecated-ui-props: Add `margin` to list + * check-deprecated-ui-props: Extend to all UI files + * app-grid: Use `{h,v}expand` instead of `expand` + * emergency-menu: Use `{h,v}expand` instead of `expand` + * lockscreen: Use `{h,v}expand` instead of `expand` + * notification-content: Use `{h,v}expand` instead of `expand` + * polkit-auth-prompt: Use `{h,v}expand` instead of `expand` + * top-panel: Use `{h,v}expand` instead of `expand` + * check-deprecated-ui-props: Add `expand` to list + + [ Antonio Marin ] + * Update Romanian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Artur S0 ] + * Update Russian translation + + [ Álvaro Burns ] + * Update Brazilian Portuguese translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Martin ] + * Update Slovenian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Andi Chandler ] + * Update British English translation + + [ Anders Jonsson ] + * Update Swedish translation + + -- Guido Günther Thu, 08 May 2025 09:09:31 +0200 + +phosh (0.46.0) experimental; urgency=medium + + [ Sam Day ] + * build: Remove nonexistent StatusPagePlaceholder symbol exports. + These were introduces in a recent commit, but the actual symbols don't + exist. + This was identified by @fossdd when building 0.46~rc1 downstream in + Alpine, where clang is being used. Seems that lld is more strict about + this than gcc is. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1217 + Fixes: f5ca0cf4 ("status-page-placeholder: Propertify child") + + [ Guido Günther ] + * build: Let plugins depend on generated enum headers. + Plugins (like dark-mode) use it. + * data: Only apply override to Phosh. + We don't want to override the default for other environments. + Fixes 24a255401 ("data: Disable lockscreen background by default") + * screenshot-manager: Exit early when saving screenshot fails. + We shouldn't try to update recent files or save a thumbnail. + Fixes 0a2228f85 ("screenshot-manager: Create thumbnails of screenshots") + * screenshot-manager: Move clipboard copying to separate function. + Pure code move. We only change the g_steal_pointer() of the pixbuf to a + g_object_ref() to keep a reference. + * screenshot-manager: Chain copy to clipboard past saving the image. + The code assumed that saving was faster than the clipboard copy opaque + timer. If things are I/O is slow the frames would be freed before the + on_save_to_pixbuf_read would complete and we'd assert. + To avoid this save to disk first and then to clipboard for the case + where both is wanted (e.g. power button long press). + Fixes ca42c89b2 ("screenshot-manager: Allow to save internal screenshots") + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1220 + + [ Sicelo A. Mhlongo ] + * rotation-manager: Do not delay marking accelerometer as released. + As soon as a request to release the claim on accelerometer is dispatched, mark + it as released in Phosh, even if a response from iio-sensor-proxy may only be + received much later. + This avoids the state inside Phosh getting out of sync with iio-sensor-proxy + after a suspend-resume cycle, for which the ReleaseAccelerometer response may + only be received after the system has resumed. + Closes #1214 + + [ Bruce Cowan ] + * Update British English translation + + [ Sebastian Krzyszkowiak ] + * stylesheet: Improve legibility of app grid and top bar. + Add subtle shadows when translucent. + * stylesheet: Improve legibility of the lockscreen on arbitrary backgrounds + * stylesheet: Improve legibility of call view on arbitrary backgrounds + + [ Vincent Chatelain ] + * Update French translation + + [ Vasil Pupkin ] + * Update Belarusian translation + + -- Guido Günther Mon, 31 Mar 2025 09:19:21 +0200 + +phosh (0.46~rc1) experimental; urgency=medium + + [ Guido Günther ] + * media-player: Bind playable property to player's can-play. + That way we also show the player when e.g. media play is stopped (rather + than paused). Let's see if players implement that property properly. + * overview: Insert app to the right if no parent is found. + We didn't mean to change the position of new apps, just that of + children. + Fixes 6b0a4d539 ("overview: Insert new children right of their parents") + * media-player: Fetch cover art via http. + Do that using an async method to not block the UI should the download + hang. + We avoided that so far but e.g. shortwave provides http URLs to radio + stations so lets do it and rather switch to glycene at some point. + * build: Swap old and new in abi compliance check. + The symbols we have checked into version control are the ones from the + old version (at least when you're not a tachyon). + * package: Update from Debian. This makes sure our packages don't file + conflict with those in downstreams and also takes the new library + versioning into account. + * packaging: Don't build gir package when not building a lib + * home: Reset overview when folded. + Resetting the overview's app grid when unfolding introduces flicker when + clearing the search (when a search term is present). This is very + visible when there are running apps. + Avoid the flicker by clearing the search when the app grid isn't + visible. + As the former `reset` now mostly refreshes the thumbnails we rename it + accordingly. + * top-panel: Set padding via css. + So far we set the margins on the top panel itself which was fine as long + as those were symmetric. If we set different margins this would shift + the clock out of center but setting the margins on the network and + indicator boxes doesn't work either as the case where we move the clock + off center needs the margins applied to the clock.` + So instead of worrying about all those cases use `:first-child` and + `last-child` css which handles this for us (at the expense of style + calculations on layout changes). + This also has the advantage that we don't need to worry about start and + end in RTL locales. + * top-panel: Introduce padding towards cutouts. + If there's a cutout in the indicator areas we use a smaller margin than + towards the screen edge. + * layout-manager: Make rectangles const. + These are the references that should stay the constant. + * layout-manager: Round float values. + We want the cutouts bounding box to be at least as large as the physical + notch. + * layout-manager: Handle cutouts in the indicator areas. + Shift elements towards the center unless that would result in massive + movement. + For smaller cutouts within the indicator boxes we'll insert stub + elements at some point in the future. + * data: Select less noisy default background + * data: Select accent color matching the back ground + * background-manager: Remove background when it goes away. + When we unplug a monitor the widget goes away due to the layer surface + being destroyed, make sure we remove it from the list of known + backgrounds as we otherwise might access stale data. + Instead of dropping on_monitor_removed completely we keep it as + consistency check. + That way it doesn't matter if the widget gets destroyed before the + output or the other way around. + * treewide: Systemd unit is for development purposes only. + Clarify that the systemd unit shouldn't be used in production. + * issue-template: Mention display manager. + Update some other parts while at that + * ci: No need to fetch phoc from sid + * build: Specify the public headers. This ensures abi-dumper doesn't + care about non public structs. It needs universal-ctags for that. + * build: Regenrate the ABI dump + * docs: Add phosh.gsettings manpage. + We'll document used gsettings here. This makes it easier to contribute + to than the wiki as everyone can make pull requests and also ensures the + shipped documentation matches the running shell. + * feedback-manager: Add emission hook for clicked and row-activated signals. + This ensures consistent feedback for all "press" related events. + * treewide: Remove most "button-pressed" emissions. + Remove those that were triggered by button presses anyway. + * packaging: Install new manpage. + This fixes the nightly build. We run with nodoc in CI so we didn't + notice. + * style: Close block. + GTK4's CSS parser catches this + * wifi-manager: Track whether a WiFi scan is going on. + Consumers can track the `scanning` property + * wifi-status-page: Show spinner when a network scan is ongoing. + This provides better feedback to the user about the ongoing + operation. + We use a callback rather than property binding as we'd need transform + functions for the stack page and related code in this class uses + callbacks too. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1138 + * wifi-status-page: Reindent UI file. + No functional change + * tests/integration: Close stdin and stdout. + This prevents to prevent the buffers to being filled and phoc entering a + blocking state. + I've been able to trigger this locally and could see phoc hanging on + writing to stdout and thus `wlr-randr` blocking as can be seen in the + failed tests: + https://gitlab.gnome.org/World/Phosh/phosh/-/jobs/4884009 + * tests: Make unit tests depend on schema. + This allows them to run without invoking any extra targets. + We rename the target as the plugins use `compile_schemas` to locate + `glib-compile-schemas`. + * ci: Drop extra step to build schema. + We fixed the dependencies in the previous commit + Fixes 3f328c7d6 ("gitlab-ci: Run unit tests under ASAN too") + * home: Fix indent + * lockscreen-manager: Modernize indent and property definitions + * lockscreen-manager: Adjust callback names according to style. + Also remove unneeded casts. + * background: Avoid paint when not rendering pixbuf. + No need to do so. + * background-cache: Allow to remove cached backgrounds selectively + * background-manager: Remove backgrounds selectively. + This will allow us to cache different backgrounds (e.g. overview + and lockscreen) + * background-cache: Make loading an async function. + Make it an async function instead of a signal so it is clear which + background got loaded. + * background: Move scale-to-min pixbuf scaling to utils. + This allows it to be used from other classes. + * lockscreen: Allow to set background image. + Make lockscreen transparent and add a layer surface carrying the + background beneath it. This ensures GTK won't be bothered at all when UI + elements move over the background. + Images are always loaded using cover style to make sure the whole + background is covered. + We only load images on the lockscreen, ignoring shields. + * lockscreen-manager: Handle picture-uri. + Setting a background image will triggering an image load using the + background-cache. If successful we set the image on the primary + lockscreen. + Background can be set via + gsettings set org.gnome.desktop.screensaver picture-uri 'file:///file/to/background.jpg' + * shell: Move updating top panel transparency to its own function. + This will become more complex + * shell: Make top bar transparent when locked. + We need to check if the top bar exists as this can be invoked early when + phosh is started locked (`-L`). + * docs: Document new settings key + * data: Add default lockscreen background + * data: Disable lockscreen background by default. + This ensures we don't pick up any accidental configuration users might + have. + * top-panel: Use icon's pixel size. Actually use the icon's pixel + size. We accidentally used the enum value. + Fixes 79808a34b ("top-panel: Provide default icon size and min padding") + * layout-manager: Assume icons don't fill the full height. + Status icons usually don't fill the full box so assuem we have 20% of + padding. + + [ Arun Mani J ] + * event-list: Simplify using `gtk-builder-tool` + * upcoming-event: Simplify using `gtk-builder-tool` + * emergency-info-prefs: Simplify using `gtk4-builder-tool` + * emergency-info-prefs: Simplify derived and `Adw` widgets + * emergency-info: Simplify using `gtk-builder-tool` + * emergency-info: Simplify derived and `Hdy` widgets + * dark-mode-quick-setting: Simplify derived and `Hdy` widgets + * home: Simplify using `gtk-builder-tool` + * lockscreen: Simplify derived and `Hdy` widgets + * notification-frame: Simplify derived and `Hdy` widgets + * polkit-auth-prompt: Simplify derived and `Hdy` widgets + * notification-frame: Simplify using `gtk-builder-tool` + * ci: Check UI files for simplification + * gvc-channel-bar: Do not hijack scroll event + `GtkScale` handles it natively, so no need for us to override. + * gvc-channel-bar: Include GTK headers. + Removes linter warnings. + * gvc-channel-bar: Simplify + * timestamp-label: Wrap under `GtkBin` + `GtkLabel` is a final class in GTK 4, so let's wrap it under `GtkBin` + which will become `AdwBin` or something similar in GTK 4. + * timestamp-label: Uncrustify + * revealer: Propertify child + * revealer: Wrap under `GtkBin` + `GtkRevealer` is a final class in GTK 4, so wrap it under a bin. + * revealer: Move UI logic to UI file + * revealer: Simplify and reformat + * top-panel: Drop assigning default values + * status-page: Allow NULL for header and footer + * status-page: Propertify child. + Makes code more readable and ready for GTK 4 as we expose methods to + directly add and remove the child (like in GTK 4 style of "child as + property"). At the same time, simplify destroy implementation by + worrying only about the widgets we get and set, the remaining will be + cleaned by GTK as they are owned by the template API. + * status-page-placeholder: Propertify child. + Makes code more readable and ready for GTK 4 as we expose methods to + directly add and remove the child (like in GTK 4 style of "child as + property"). At the same time, simplify destroy implementation by + worrying only about the widgets we get and set, the remaining will be + cleaned by GTK as they are owned by the template API. + * status-page-placeholder: Simplify. + Reorder the virtual method implementations like elsewhere. + * quick-setting: Propertify child. + Makes code more readable and ready for GTK 4 as we expose methods to + directly add and remove the child (like in GTK 4 style of "child as property"). + * quick-setting: Cleanup and reformat. + Use most specific type where sensible. + * quick-settings-box: Expose methods to add and remove. + Makes code more readable and ready for GTK 4 as we expose methods to + directly add and remove the children instead of relying on + `GtkContainer` APIs. + * quick-settings-box: Cleanup and reformat. + Use most specific type where sensible. + * app-grid-base-button: Propertify child. + Makes code more readable and ready for GTK 4 as we expose methods to + directly add and remove the child (like in GTK 4 style of "child as property"). + * app-grid-base-button: Uncrustify + * hacking: Add notes about derivable parent widgets + * status-icon: Move UI logic to UI file + * status-icon: Uncrustify + * revealer: Check for child type + * revealer: Use setter function in destroy + * status-icon: NULLify `extra_widget` in destroy + * app-grid-button: Use gesture to handle right click. + Also remove `popup_menu` virtual method implementation as it is not + available in GTK 4 and handled by gestures already. + * app-grid-button: Move long-press gesture to UI file + * app-grid-button: Prefix action name with group. + The equivalent method in GTK 4 to bind a model to a popover takes no + namespace parameter, so let's make the + current code ready for it. + * app-grid-button: Cleanup. + Rename variables and methods to match the style guide. + * app-grid-button: Uncrustify + * app-grid: Force the box to align start vertically. + Fixes the case where the box gets more-than-required allocation and so decides to + distribute it among the children, causing odd spacing among them. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1208 + * activity: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * ambient: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * app-auth-prompt: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * app-grid: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * app-grid-standalone: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * background: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * background-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * cell-broadcast-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * drag-surface: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * emergency-calls-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * emergency-menu: Use `gtk_widget_set_visible` + `gtk_widget_show_all` is unavailable in GTK 4, so let us use the + proper setter method. + * end-session-dialog: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * fading-label: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * gnome-shell-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * gtk-mount-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * gtk-mount-prompt-prompt: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * keypad: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * layer-surface: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * lockshield: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * lockscreen-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * lockscreen: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * media-player: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * mount-operation: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * network-auth-prompt: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * notification-content: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * osd-window: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * overview: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * polkit-auth-prompt: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * portal-access-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * power-menu-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * proximity: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * run-command-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * screenshot-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * session-manager: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * settings: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * shell: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * system-modal: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * system-modal-dialog: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * top-panel: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * widget-box: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * wwan-info: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * emergency-info: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * custom-quick-settings-standalone: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * notify-server-standalone: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * widget-box-standalone: Use `gtk_widget_set_visible` + `gtk_widget_{show, hide}` are deprecated in GTK 4, so let us use the + proper setter method. + * quick-settings-box: Add method to hide status. + Can be used to hide status externally, like by settings. + * quick-settings: Add method to hide status. + Can be used to hide status externally, like by settings. + * settings: Add method to hide details. + Hides quick-settings box status-page and audio-settings details. + * top-panel: Hide settings' details on fold. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1136 + * settings: Use `hide_details` on panel launch. + Hides quick-settings box status in-addition to audio-settings details. + + [ Evangelos Ribeiro Tzaras ] + * overview: Only look up carousel position if parent is found. + This saves a few cpu cycles. + + [ Sam Day ] + * status-icon: Remove obsolete show_always getter+setter. + This property was dropped back in 2020. These names managed to sneak + their way into the symbol export map, but since the actual symbols don't + exist this doesn't break ABI compatibility (or at least, + abi-compliance-checker did not complain) + Fixes: 1f9f31db ("status-icon: Drop show-always") + * ci: Always publish ABI check artifacts. + If there's an ABI breakage the pipeline will fail (which is good). This + change ensures that when this happens the report containing detailed + info is made available. + + [ Bardia Moshiri ] + * util: Move sys/mman and fnctl out of MEMFD ifdef + these are needed if in meson we set have_memfd_create to false. + + [ Gotam Gorabh ] + * notification: Don't focus the notification-frame. + Don't focus the notification-frame as it overlap the + action buttons. However notification body still can + focus without shadow. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1152 + * notification-frame: Add slide-down animation. + Add slide-down animation to rest of the notifications after a new + notification appears. + We skip the animation when it currently isn't visible anyway. + + [ Sebastian Krzyszkowiak ] + * activity: Fix thumbnail scaling. + PhoshActivity is unaware of GtkButton's padding. Move it + to GtkDrawingArea, so the thumbnail size can be calculated + with correct aspect ratio. + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Andika Triwidada ] + * Update Indonesian translation + + -- Guido Günther Fri, 21 Mar 2025 18:19:23 +0100 + +phosh (0.45.0) experimental; urgency=medium + + [ Guido Günther ] + * build-symbols: Robustify. Exit on errors and add end of here file (EOF). + * build: Simplify path to inputs a bit. + This is easier to read but also make sure we don't get the + directories added twice when built as a subproject. + * phosh-session: Make phoc startup verbose. This gives us some important + information like used renderer, backend, etc. We require phoc 0.45.0 for + that. + + [ Sam Day ] + * build: Bump minimum Meson version to 1.2.0. + We need this to set override_options on both_libraries (to enable DWARF + debug symbols when performing ABI compliance checks) + * build: Add ABI compliance check. + When the abi-check option is enabled, abi-dumper is run on the + shared library, and then abi-compliance-checker compares this result to + the baseline in src/libphosh-abi.dump + * build: Check in initial libphosh ABI dump. + We're starting our baseline from 0.45.beta1 + * ci: Add abi-compliance-checker job. + This job makes use of the new abi-check build option, and saves an + artifact with the new ABI dump and the HTML report that was generated + comparing it to the currently checked in baseline. + It will run automatically on tags and changes to NEWS or the libphosh + symbol list. It can also be run manually. + See: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1188 + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Anders Jonsson ] + * Update Swedish translation + + -- Guido Günther Sat, 15 Feb 2025 12:00:47 +0100 + +phosh (0.45~rc1) experimental; urgency=medium + + [ Arun Mani J ] + * Simplify more UI files + + [ Guido Günther ] + * phosh-session: Run through systemd-cat when available. + This helps to get proper debug output out of phoc under display manager + using greetd (like phog and phrog). + * notification: Add sound-file property. Needed to play custom sounds on + notifications + * notify-feedback: Pass custom sound file if present. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1184 + * notification-manager: Add and handle sound capability. + Handle `suppress-sound` by lowering the feedback level to quiet and set + `sound-file` if present in the notification. + * ci: Pull libfeedback from Debian unstable + * build: Generate dynamic list. This will be useful when we generate a + symbols file too + * build: Build symbols file. Use the information from our dyn list to create + a symbols file for the shared library. + * ci: Use newer imagemagick + * ci: Update images to v0.0.2025-02-07 + + [ Sam Day ] + * build: Manage libphosh_api_version independently from project version. + Since we're aiming to keep the libphosh API stable, the libversion + variable is renamed to libphosh_api_version, and its value is managed + manually. It will only be incremented when the API changes. + See: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1188 + * lockscreen-manager: Drop unused get/set timeout prototypes. + These functions were dropped back in 2020 by 084d406c + * layersurface: Add vtable padding to class. + This type is exported by libphosh, make sure we can evolve the vtable + later if needed. + * layersurface: Split out private header. + We just need the type available to API consumers, let's not make + anything else public for now. + * build: Remove wl protos from public API altogether. + This was introduced recently in 68bf9792, but as it turns out, we don't + need to leak the layer-shell-unstable wl proto header into the public + API after all. This is because the previous commit reduced the exports + of PhoshLayerSurface to just its type information. + * build: Introduce libphosh symbol list. + This list specifies every symbol that is defined in libphosh.h. It will + be included in the libphosh symbol map in the following commit. + * build: Combine libphosh+plugin symbol into symbol map + libphosh.so needs to export both the public API symbols and the extra + symbols needed by plugins (as libphosh consumers might instantiate a + Shell that tries to load the default in-tree plugins). + * build: Build libphosh library with symbol map. + This significantly reduces the number of global symbols exported from + libphosh.so from ~2000 to ~150. + * ci: Install abi-compliance-checker in Debian image. + A subsequent commit will introduce a CI job that uses this tool to + generate a report on the changes to API/ABI compatibility for the + libphosh shared library (and later, the phosh executable too). + It introduces about a half-dozen transitive dependencies and ~8mb + additional installed size. This seems acceptable and preferable to + introducing a whole additional sub-image. + * ci: Fix double line continuation token in debian.Dockerfile. + Podman seems okay with this, but it makes Docker upset. + + [ Artur S0 ] + * Update Russian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + -- Guido Günther Fri, 07 Feb 2025 19:07:14 +0100 + +phosh (0.45~beta1) experimental; urgency=medium + + [ Guido Günther ] + * fake-clock: Make WallClockMock available in regular shell + * main: Allow to use fake clock. + Allow to use the fake clock in the regular shell. This eases making + demos and screenshots and it doesn't add much code. + * docs: Document PHOSH_FAKE_CLOCK + * screenshot-manager: Add cancellable. + Always good to cancel async operations on shutdown + * screenshot-manager: Separate hours, minutes and seconds by ':' + Rather than using '-' for date and time, separate the time by ':' to + make it easier to identify. + * screenshot-manager: Create thumbnails of screenshots. + Screenshots are used a lot on mobile so make sure we generate thumbnails + for the cases where the e.g. uploading application doesn't. + We already have all the data so write out a thumbnail per + https://specifications.freedesktop.org/thumbnail-spec/0.8.0/ + * wwan-manager: Enable and disable autoconnect. + When we disable a WWan connection we should also disable autoconnect as + otherwise the connection comes back up after resume or reboot. + Hence we need to reenable autoconnect when the connection is activated + again. + Closes: https://gitlab.gnome.org/guidog/meta-phosh/-/issues/16 + * wwan-mm: Use swapped signal. Our handler wants `self` first + Fixes 51da91508 ("wwan-mm: Switch to MM objects") + * ci: No need to fetch phoc from sid. The one in trixie is recent enough now + * gitignore: Ignore pycache files + * home: Use g_timeout_add_once() Simpler API + * home: Queue draw instead of committing the surface. + We don't know if GTK already attached a matching buffer and attaching an + incorrectly sized one can lead to the compositor terminating the + connection so let GTK decide when to commit. + * top-panel: Queue draw instead of committing the surface. + We don't know if GTK already attached a matching buffer and attaching an + incorrectly sized one can lead to the compositor terminating the + connection so let GTK decide when to commit. + * packaging: Add pytest. Useful to run the tests + * packaging: Require newer phoc. This ensures we have a recent enough + one in the tests + * packaging: Depend on wlr-randr. We use it in the tests + * tests: Add simple test to check mode changes. + We hade some crashes in that area so make that testable in CI. + This can also be extended to check the binaries on the installed system + at some point thus making it easier for distros to validate their + builds. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1171 + * ci: Run integration tests + * ci: Drop phosh startup tests. + With the pytests we spawn a shell too. It served us well up to now. + * layersurface: Drop protocol version check. + We require recent enough phoc anyway. + * treewide: Assume phoc supports alpha layer surfaces. + This is supported by phoc since May 2023 + * layersurface: Drop now unused has_alpha() + * layersurface: Simplify map/unmap + map()'s and unmap()'s are impls for virtual methods so + we can't end up with an incorrectly casted type (as e.g. + in signal handlers). This is a left over from: + Fixes 953894a2b ("layersurface: Use class methods instead of signals for map and realize") + * layersurface: Allow to set alpha before the surface is mapped. + Catch up with values set prior to mapping the surface on map. + So far this resulted in errors but allowing it makes things more + flexible as we don't need to bother when the surface gets mapped. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/940 + * layersurface: Allow to set stacking before the surface is mapped. + Catch up with values set prior to mapping the surface on map. + So far this resulted in errors but allowing it makes things more + flexible as we don't need to bother when the surface gets mapped. + We take a weak ref on the target surface so we can notice when it goes + away. Otherwise we would try to bind a non existent target surface when + the target surface goes away before the surface itself gets ever mapped. + * home: Bind background visibility. + Instead of always showing it, bind it to the visibility of home. + This ensures they stay in sync. + Since `visible` is false in the constructor this is a slight behavior + change as the background will now become visible a bit later. + * top-panel: Bind background visibility. + Instead of always showing it, bind it to the visibility of top panel. + This ensures they stay in sync. + Since `visible` is false in the constructor this is a slight behavior + change as the background will now become visible a bit later. + * system-modal-dialog: Rename obj to object. + We have a mix in the code base but 'object' is more prevalent. + * system-modal-dialog: Fade in system modal dialogs. + This is a bit less surprising and with the cleanups in the layer surface + code we can fade in with a simple PhoshAnimation. Since the compositor + drives the rendering this doesn't cause much CPU (as GTK based blending + would). + * background-manager: Update background when dark uri changes too. + We tracked the regular background but not one for the dark theme. + * background: Fill background with primary color. + This makes sure we have a proper background in the overview in + case the selected image has transparency. + In case of a missing image it ensures we fall back to black so the + overview always gets a non transparent background. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1173 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1169 + * background: Use background color for the primary output's background. + The primary output's background determines the color behind the top and + bottom bars when they are folded (since the actual bars are + transparent). So use the @phosh_bg_color CSS value for these areas + rather then the primary-color gsetting. + This ensures the seamless fading from the opened overview to the black + top and bottom panel works even for situations where the primary-color + gsetting is not '#000000'. + * media-player: Don't access `self` when call got cancelled. + The memory has then been free'd already. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1176 + Tested-by: Fiona Klute + * media-player: Simplify flow. + We first handle the success then the failure path. By basing this on + `var` we avoid `else if`. + Tested-by: Fiona Klute + * ci: Pull phoc from Debian unstable. + Should fix the crash we're seeing on phoc shutdown in CI + * osk-button: Drop widget. It's not used anymore. + Fixes: fe308c2ce ("home: Shrink home-bar height and move osk activation to center") + * shell: Allow plugins to get the primary and built-in monitor + * monitor-manager: Allow to set a monitor's scale. This is similar to + setting the transform. + * head: Move scale calculation for video modes to util. + It's not bound to the actual head we use so we can also use it e.g. for + monitors. + * build: Export symbols needed to set monitor scale + * tests/integration: Honor WLR_BACKENDS from environment + * docs: Use full line length. Matches what we do in the code + * docs: Move callbacks one indent level up. Doesn't belong to properties + * docs: Add signal example + * docs: Add since annotations to examples + * docs: Add doc string + * docs: Add GObject introspection docstring example + * manager: Use idle_add_once variant. A bit less code and easier to read. + * notification: Allow for empty do_action. Shell internal notifications + only listen to signals so they don't have `do_action` implemented + * build: Move wifi-manager to tool sources. + We want to add the connectivity manager there and as this will reference + the wifi-manager we need to move things over (which is an improvement + over all as we want to empty `libphosh_sources` in favor of + `libphosh_tool_sources`). + * connectivity-manager: New class to track connectivity information. + This will be used for detecting captive portals and by + connectivity-info. + * connectivity-info: Use connectivity-manager + * connectivity-manager: Only update on state changes + * connectivity-manager: Show notification when we detect a captive portal. + Clicking on the portal opens the portal URL in the browser + * d/control: Recommend network-manager-config-connectivity-debian. + This allows NMs captive portal detection to work. + * layersurface: Fix initial alpha sync. We want to set for alpha != 1.0 + Fixes 6b2d9f6b6 ("layersurface: Allow to set alpha before the surface is mapped") + * layersurface: Clear effects on unmap. + This allows us to toggle visibility on and off as the effects will be + recreated on map. + * home: Recolor only the home bar on state changes. + So far we added the style class to the whole home widget which means + recalculating the style for all widgets (including the app grid). This + way we need to calculate way less new CSS. + In order to keep HighContrast theme working we restyle the whole Home + widget on theme changes still. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1177 + Tested-by: Andrey Skvortsov + * home: Toggle background widget on/off based on visibility. + This avoids showing the background for a split second in high contrast + mode and also saves computation time. + * wifi-network-row: Use property setter. + Instead of splitting setting and binding make it look like a regular + property setter elsewhere. + * bt-device-row: Name async callback _ready. + Following our usual pattern + * quick-setting-box: Don't warn when closing "wrong" status page. + Don't emit a warning when trying to close a status page that isn't + currently open. This allows quick settings to emit `hide-status` + without keeping track whether their status it the one currently shown. + * bt-device-row: Emit signals for activation. + Emit a signal when activation starts (and hence the row is "busy") and + when it finishes. This allows the status page to keep track of the rows + state. + * status-page: Add "done" signal. + Status pages should emit this when they want to be closed + * bt-status-page: Emit done when last device activated successfully. + We emit the signal when all devices finished activating and the last one + finished successfully as the user is very likely done with the status + page at this point. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1166 + * quick-setting: Hide status page on "done" signal. + When the status page indicates it is done, close it. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1166 + * scaling-quick-setting: Close status page on scale changes. + Let status page emit "done" signal when setting the scale. This closes + the status page. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1166 + * wifi-status-page: Use g_connect_object() for wifi-manager. + Less code and we'll connect more. Fix one trailing white space while at + that. + * wifi-network: Clarify ownership of phosh_wifi_network_get_ssid() + It is transfer: none. + * wifi-status-page: Fix incorrect signal handler signature. + There's no `GParamSpec` in the "clicked" signal. Thankfully we're not + using the button yet so this wasn't a problem so far. + * wifi-status-page: Close status page when connection succeeds. + Close the status page when the Wi-Fi network the user selected gets + activated. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1166 + * dir-locals: Set indicator column. + Makes indenting a bit simpler + * notification: Clear expiry timeout before setting it. + This allows to bump the timeout of an existing notification + * tests: Stub wifi-manager with an object. + This allows the connectivity test to retrieve it + * shell: Create Wi-Fi manager upfront. + This ensures we have it around when needed. + * shell: Create connectivity manager upfront. + This ensures correct ordering between Wi-Fi and Connectivity manager. + * connectivity-manager: Make notification persistent. + We keep the notification around unless the SSID of the network changes + or we get connectivity again. Close it as soon as we get connectivity + again or the SSID changes. + The banner will close after 20s (rather than 10s atm) so it is a bit + easier to catch. + * connectivity-manager: Make notification silent. + No need to buzz or blink when we detect a Wi-Fi portal. + + [ Cédric Bellegarde ] + * screen-saver-manager: Connect logind prepare-for-sleep sooner + + [ Sam Day ] + * build: Add libappstream dependency. + This is needed by the PhoshMetainfoCache to be introduced in the + following commit. + * metainfo-cache: Introduce PhoshMetainfoCache. + This was originally authored by Guido in the WIP MR !950. + It handles initializing an AppStream cache "Pool", which is populated + with information about available applications on the system (installed + or otherwise). + It will be utilized in the following commit of this series, to determine + the correct AppStream "data id" that is used when invoking + gnome-software --uninstallation + * app-grid-button: Add Uninstall action + gnome-software supports an --uninstall invocation, which navigates + directly to the details page for the app and pops the prompt to + uninstall that app. + * shell: Allow overview to be hidden. + This commit introduces a PhoshShell::overview-visible property, which is + bound to the visibility of the PhoshHome that the shell constructs. + This property isn't used in phosh-session and probably never will be, + it's for custom PhoshShell subtypes, such as phrog, which will use it + when displaying a "first-run" application in the system greeter view. + * shell: Minor uncrustification. + A handful of tiny cleanups in the property set/get functions, since I'm + in the area with the previous commit. + * build: Rename most libphosh_* Meson variables to phosh_* + libphosh API is about to be dramatically reduced. As such it's not + really accurate to refer to any of these sources or headers as + "libphosh", since they're meant to be internal. + * build: Omit libphosh_headers from phosh_sources. + The phosh_sources variable should only include .c source files. This way + it can be safely included in the files fed into g-ir-scanner without + causing unintended additions to introspection output. + * build: Omit phosh_tool_headers from phosh_tool_sources. + * build: Only add phosh_*_headers to phosh_headers. + * build: Generate a subset of enums for libphosh + * lockscreen-manager: Minor uncrustification + * lockscreen-manager: Split out a private header. + The constructor is moved to the private header so that PhoshCallsManager + can be excluded from the public API. + * lockscreen: Split out a private header. + The constructor is moved to the private header so that PhoshCallsManager + can be excluded from the public API. + libphosh consumers don't need to use this constructor directly, since + they can control what Lockscreen subclass is constructed via + PhoshShell's get_lockscreen_type vfunc. + * shell: Split a public header out from plugin-shell. + In the case of PhoshShell, we have two kinds of "public" - stuff that + should be exported to libphosh consumers, and stuff that should be + available to bundled plugin.so libraries. + So the header that was formerly shell.h is now shell-priv.h, and a new + shell.h header is introduced that splits the basic type definition and + some initial public methods out from plugin-shell.h + plugin-shell.h includes shell.h, and shell-priv.h includes + plugin-shell.h. Got it? Nope? Don't worry - it'll make sense eventually. + * plugin-shell: Make phosh_shell_new public API + libphosh consumers should be able to construct a PhoshShell + * plugin-shell: Make phosh_shell_fade_out public API. + This is used by phrog to do a nice fadeout of the greeter session when a + user has successfully logged in. + * plugin-shell: Make phosh_shell_get_lockscreen_manager public API. + This is useful for libphosh consumers (like phrog) to obtain a reference + to the active Lockscreen. + * plugin-shell: Make phosh_shell_get_usable_area public API + phrog currently uses this in tests to center the virtual mouse pointer. + I was going to switch that code to use Gdk but Guido indicated this + might be useful elsewhere so might as well make/keep it public. + * plugin-shell: Make phosh_shell_get_screenshot_manager public API. + This is already used directly in the libphosh-rs hello-world example, so + it seems a safe bet this should be considered public API. + * build: Keep track of public dbus sources/headers. + When codegenning the dbus proxies, we keep track of the files that + should be included in the public API of libphosh. + The newly introduced libphosh_generated_dbus_* variables are not + being used yet, but they will be when the GIR is updated to use a much + smaller subset of files for introspection. + * build: Dramatically reduce the size of libphosh public API. + The dozen or so commits prior to this one in this series were all + working towards this point. The GIR and libphosh.h are reworked to only + include a very small set of headers for the current minimum viable + public API (based on existing usage in libphosh-rs examples and phrog). + The GIR has been shrunk from ~70k lines to ~2.3k, and the number of + files installed to /usr/include/libphosh-N.NN has reduced from ~200 down + to ~20. + * build: Remove many generated+supporting headers from public API. + Now that libphosh.h is significantly smaller in scope, none of these + headers need to be included alongside the libphosh headers. + * build: Ensure wl protos referenced by public API are installed. + The previous commit was a little overzealous in removing headers from + the public API header dir. Specifically, layersurface.h is including the + generated wlr-layer-shell-unstable-v1 header. + So, similar to the earlier commit that distinguishes public dbus + generated sources from internal ones, we introduce a list of generated + wl proto headers that should be included in the public API. + * quick-setting: Add vtable padding for future API/ABI compatibility + * status-icon: Uncrustify _PhoshStatusIconClass + * status-icon: Add vtable padding for future API/ABI compatibility + * status-page: Add vtable padding for future API/ABI compatibility + * wall-clock: Uncrustify _PhoshWallClockClass + * wall-clock: Add vtable padding for future API/ABI compatibility + * treewide: Remove various docstring symbol links to internal classes. + * docs: Qualify API docs as 'public' in README + + [ Adam Honse ] + * plugins: Screen scaling quick setting plugin. + Allow to select the scale for the primary output. The list of scales + is calculated the same way we do it when presenting them to Settings. + For the moment we disable the plugin when there's more than one output + as it's not obvious to the user if it should affect the primary or + built-in display. + Clicking on the button swaps back to the previously selected scale which + looks like the best default action. + + [ Gotam Gorabh ] + * tree: Bump gtk version in ui files. + Bump gtk dependency to 3.24 and add libhandy + as a requirement where it needed. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1181 + * d/control: Add libgmobile-dev dependency + * emergency-info-prefs: Remove useless code + * emergency-info-prefs: Switch to AdwEntryRow for add_emer_contact_dialog + * emergency-info-prefs: Port add_emer_contact_dialog to AdwDialog. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1159 + * emergency-info-prefs: Port remaining GtkEntry to AdwEntryRow. + As we already make `add_emer_contact_dialog` to use AdwEntryRow + in https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1615 , + this mr will port remaining ones. + + [ Evangelos Ribeiro Tzaras ] + * calendar: Respect week number setting (Closes: #989) + * ci: Add gsettings-desktop-schemas as build dependency for alpine + + [ Arun Mani J ] + * status-pages: Disable vertical homogeneous. + These pages have a lot of stack pages of different heights. With + homogeneous, smaller pages have an awkward space at bottom. May be not + really awkward, but at least pages now take only the height they need. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1139 + * plugins: Add missing schemas compilation steps + * tools: Add missing plugins' schemas + * plugins: Build schema only on dependency change + * Simplify many UI files using `gtk-builder-tool` + + [ Sabri Ünal ] + * Update Turkish translation + + [ Martin ] + * Update Slovenian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + [ Artur S0 ] + * Update Russian translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + -- Guido Günther Sun, 02 Feb 2025 09:14:35 +0100 + +phosh (0.44.0) experimental; urgency=medium + + [ Guido Günther ] + * notification: Port to PhoshAnimation. + This simplifies the code and will make adding a slide-up animation more + straight forward. + We move the start of the animation to the map vfunc as PhoshAnimation + requires the widget to be mapped. + * app-grid-base-button: Fix indent + * system-modal-dialog: Don't process key event directly. + Less code and helps the GTK4 migration. Also makes sure we don't + interfere with other event handling. + Can be reworked to use gtk_widget_class_add_binding_action then. + * top-panel: Don't process key event directly. + Less code and helps the GTK4 migration. As we don't have a "closed" + signal we'll use "activated" which is enough to make `PhoshShell` toggle + fold. + Can be reworked to use gtk_widget_class_add_binding_action then. + * top-panel: Move settings upwards. + As we don't have a dedictated "close" signal "activate" will do. + This allows us to drop the `packaging` elements which eases the reading + flow and eases the GTK4 transition. + * app-grid-base-button: Make abstract. + It can only serve as base class for other buttons + * app-grid-base-button: Remove css name. + We style the derived classes. + * app-grid-button: Use alpha on activation and search too. + Amends df0f30580 ("app-grid-button: Use alpha when focused") + * app-grid-folder-button: Let style match the app-grid-button. + We use a slightly darker style to make the activation visible + Amends df0f30580 ("app-grid-button: Use alpha when focused") + * app-grid: Add outline around selected search item. + Makes it easier to identify when using a background + * build: Make cell-broadcast headers private + * shell: Drop unused header + * packaging: Lower phoc dependency. + 0.40~rc1 is recent enough + * ticket-box: Port prefs to GtkFileDialog. + This gives us the portal which is adaptive. + * splash-manager: Use GDesktop's enum. + It has been released a while ago. + * mode-manager: Drop unused defines + * home: Indicate transitioning state. + Indicate when the home is transitioning between folded and unfolded. + * top-panel: Set background on top bar. + This avoids showing regular windows under the top bar which can be + confusing. + While this is not very visible in auto-maximize mode it can be seen in + floating mode or when e.g. revealing the shell over fullscreen windows. + We add an outer container with background to the top bar so we can keep + setting the margins on the top-bar itself. + The alternatives (using padding or to setting the marign on the inner + elements) are more complicated as we move elements around due to notch + avoidance and the margin also changes due to that. We would thus need to + generate style classes on the fly or determine the elements dynamically + which is more fragile. + We can make things slightly nicer by setting the solid + background a bit later (when the splash closes) but let's do that with + when zooming in the splash + (https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1264) + as this will supply the necessary plumbing to phosh shell. + + [ Gotam Gorabh ] + * notification-banner: Add slide-up animation to banner. + Add slide-up animation to the notification-banner so that + it will disappear with slide-up animation. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/377 + * notification-frame: Don't return bool for notify signal handler + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Alexandre Franke ] + * Update French translation + + [ twlvnn kraftwerk ] + * Update Bulgarian translation + + -- Guido Günther Tue, 31 Dec 2024 09:25:07 +0100 + +phosh (0.44~rc1) experimental; urgency=medium + + [ Guido Günther ] + * po: Update app-id. No need to break all the translations + * thumbnail: Use gpointer. We avoid "void *" in other parts of the codebase too + * activity: Simplify UI. Drop properties at their defaults by using + gtk-builder-tool. We also already split expand to {h,v}expand. + * activity: Drop redundant CSS. No need to set padding and transition twice + in the same block + * toplevel: Allow to (un)fullscreen toplevels + * activity: Add button to unfullscreen. We show it when the activity is + fullscreen and emit a signal when it should be unfullscreened. + * activity: Activate unfullscreened app. When the user unfullscreens an app + they likely also want to activate it so do this too. + * overview: Unfullscreen toplevel when activity requests it + * ci: Simplify adding packages from unstable. This makes it easy to e.g. + fetch phoc from sid when needed. + * top-panel: Set css class at the end. This makes it match other class_init + layouts + * layersurface: Add check for valid alpha values + * layersurface: Make layer property 'construct'. This allows us to get it in + constructors. + * protocols/layer-shell-effects: Update to latest version + * layersurface: Add support for stacking surfaces. Use layer-shell-effects + version 3 to allow stacking layer surfaces above/below each other. + * layersurface: Add getters for output and layer + * settings: Add a background. This improves readability when the top-panel + itself is transparent. + * background: Allow to set the layer + * css: Ease toggling background on/off. We add a "solid" style class for + that. + * top-panel-bg: New widget that is the top panel's background. + We use a separate widget so we can drive alpha blending on the + compositor side. + * top-panel: Use a background. + This allows us to tweak the top-panel's transparency based on how far + the surface is folded. + We use that to make it fully transparent (and hence the wallpaper shine + through) when the overview is unfolded or when on the lockscreen. + * home: Add a background. + Add the desktop's background to home so mobile users can enjoy it too. + Adjust the background's transparency based on how far the home surface + is pulled out. + * activity: Use simpler phosh_util_toggle_style_class + * activity: Return early if we have no surface + * activity: Set background in drawing area. + This ensures thumbnails don't look that cut off when the OSK is + unfolded. + We only do this for maximized windows to avoid the background on e.g. + dialogs. + We should restrict this to toplevels without a parent once + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1278 + lands + * app-grid-button: Use alpha when focused. + This avoids a dark black square which looks out of place on colored + backgrounds. + * style-manager: Add theme-name property and methods to get style information. + We want other parts of the shell to track theme changes so add a + property for this plus the needed getters. + * home: Handle HighContrast theme. We don't want to reduce contrast in the + overview in this case. + * top-panel: Handle HighContrast theme. Ensure we have a solid background + when HighContrast is active + * data: Prefer dark theme. + We have a quick setting to toggle it off if needed and it better fits + the style changes for background images. + * packaging: Conflict with older p-m-s. Add a conflict to p-m-s that doesn't + use AdwPreferencesDialog for plugin prefs yet. + * overview: Insert new children right of their parents. + So far we appended to the very end of the list which made dialogs + belonging to a parent look out of place. + * wwan/ofono: Avoid GAsyncReadyCallback casts. + Rather cast the user data argument which gives us a type check and fewer + lines of code instead. + * data: Move schema to mobi.phosh too. + We keep the old schema paths to not break existing setups. + * data: Move enums into canonical namespace + * docs: Update schema locations + * system-modal-dialog: Allow to get dialog title. + This makes it consistent with the setter + * wwan-mm: Switch to MM objects. + This frees us from creating the proxies manually and gives + somewhat nicer function names. + * wwan-manager: Add signal to notify about new CBMs. + The wwan manager should able to notify other parts of the shell that a + new cell broadcast message was received. + * wwan-mm: Track new CBMs. + Track new cell broadcast messages and emit 'new-cbm' when + the received message is complete + This needs a recent ModemManager with Cell Broadcast support + so we compile that in conditionally based on whether we find + the necessary API in libmm-glib. We can drop this once this + feature made it into a released MM version. + * data: Add setting for cell broadcast messages + * cell-broadcast-prompt: New dialog for cell broadcast message + * cell-broadcast-manager: New class handle display of CBM information. + Acts on signals from the WWAN stack to show cell broadcast messages + (CBMs) as system modal dialogs or (later on) notifications. + * background-manager: Track background file changes. + The portals use the same name for the background file so we + want to track when the file content changes. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1012 + * build: Bump libsoup dep. + Now that there's a stable release with + https://gitlab.gnome.org/GNOME/libsoup/-/merge_requests/394/diffs + just depend on that. + * docs: Drop submodule init. + We're using subproject wraps since some time + * docs: Be less L5 specific + * docs: Fix path to session script. + Fixes ea33170db ("data: Rename phosh startup script to phosh-session") + * brightness: Use automatic cleanup + * overview: Bind template vars past signals. + This matches the class_init layout in other classes. Sort alphabetically + while at that and use our current indent. + * app-grid: Use g_timeout_add_once. Simpler and less error prone + * app-grid: Avoid expand packaging for search. + That won't work with GTK4 and we can has hexpand just as well + * app-grid: Prefix signal handlers. + Make them match our `on_` style + * app-grid: Bind template vars past signals. + This matches the class_init layout in other classes. Also use our + current indent. + * notification-banner: Use minimal size for banner. + Size the banner based on the allocation. This ensures areas around the + notification will still remain touchable. + We keep a min width of 360px to ensure the notification doesn't get too + narrow. + Fixes: c0b37ad5c ("notification-banner: Drop unused hard coded values") + * tests/notification-frame: Don't leak actions. + Fixes fa92b3cbd ("tests/test-notification-frame: Add testcase for + need_separator function") + * shell: Use consistent resource prefix. + We could use the longer /mobi/phosh/shell but let's use the shorter + /mobi/phosh as these don't leak outside the binary. + * session: Move to phosh.mobi. + Move desktop file and icons as well + * debian: Drop unused files + * portal: Move to consistent prefix. + Use mobi.phosh.Shell here too + * build: Switch app-id to mobi.phosh. + Fix the fallout in the calendarserver and the upcoming events plugin + triggered by that. + * portals: Add 'shell' to portal name. + We only provide the access portal so add 'shell' name and free the + namespace for the actual portal so we don't have to use 'pmp' for it. + * portals: Use phosh as portal name. + Use the canonical name for the portal implementation + * packaging: Conflict with older phosh portal. + This ensures we don't run against a portal that is still + named pmp + + [ Gotam Gorabh ] + * plugin prefs: Switch to AdwPreferencesDialog. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1515 + * tools/plugin-prefs-standalone: Adapt the AdwPreferencesDialog plugin prefs. + See: https://gitlab.gnome.org/World/Phosh/phosh-mobile-settings/-/issues/64 + * notification-frame: Add separator for default-action notification. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/901 + * tools/check-notification: Cover more cases + * notification-frame: Don't add separator to row at index 0 + * tests/test-notification-frame: Add testcase for need_separator function + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Tim Sabsch ] + * Update German translation + + [ Pierre Michel Augustin ] + * Update Haitian Creole translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + -- Guido Günther Sat, 07 Dec 2024 23:44:41 +0100 + +phosh (0.43.0) experimental; urgency=medium + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Evangelos Ribeiro Tzaras ] + * end-session-dialog: Wait 60 seconds (not 61) before auto-confirming. + This source is added via g_timeout_add(). As a second has already passed, + decrement first and then update the dialog text. + + -- Guido Günther Fri, 15 Nov 2024 13:44:54 +0100 + +phosh (0.43~rc1) experimental; urgency=medium + + [ Gotam Gorabh ] + * docs: Update commit message guidelines for bug fixes + * notification-frame: Add slide-up animation to the notifications. + Add slide-up animation to the rest of the notifications + into the space that the one that was just dismissed. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/788 + + [ Guido Günther ] + * build: Drop the versions block. + Not needed anymore. We require those versions anyway. + * build: Install gir when we create the bindngs lib. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1118 + * notify-feedback: Send event for new notifications. + The feedback theme should decide what should happen on notifications, + not the shell. + * notify-feedback: Make condition match maybe_trigger_feedback. + The early exit should use the same criteria we use in + `maybe_trigger_feedback` later on (as it's only purpose is to shortcut + the logic early. + * notify-feedback: Emit proper feedback when device is locked. + So far we only picked the `*-missed-*` events and relied on the app to + submit proper feedback. That isn't very reasonable though as the app + e.g. won't know if the user has disabled notifications. + So emit feedback for all notifications with reasonable categories. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/728 + Closes: https://source.puri.sm/Librem5/feedbackd/-/merge_requests/69 + * notify-feedback: Allow apps to special case SMS. This is useful for + phones + * notify-feedback: Clarify event purpose. We only track the events for the + inactive case + * notify-feedback: Always trigger feedback when events are added. Since we + handle inactive and active now we shouldn't silently ignore new + notifications. + * notify-feedback: Handle feedback for active case separately. + This ensures we don't forget to end feedback for long running + feedback when the device is active (e.g. `call-unanswered`). + * style-manager: New class to handle themes and styles. + We move the stylesheet selection into a separate class so we can add + properties/signals and also more style settings for the upcoming accent + color support. + * notify-manager: Allow `transient` hint to be int. + There's no bool so people use + notify-send --hint=int:transient:1 -t 3000 subject body + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/351 + * util: Handle MFD_NOEXEC_SEAL. + This got added in 6.3. See https://lwn.net/Articles/918106/ + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/928 + * libphosh: Add missing classes. This unbreaks the Rust bindings + * plugins: Simplify schema compilation. Add a validation while at that + * run: Add missing schema dirs + * tools/plugin-prefs: Only load one extension point at a time. + Quick setting plugins and lockscreen plugins use different entry points + so loading all prefs leads to warnings. Allow to select the plugin type + to use instead. + Fixes b8589e6df ("tools: Add widget to test custom quick settings") + * notify-manager: Make shell notifications more flexible. + Allow to pass a notification object. This gives more flexible on e.g. + the icons used. + * build: Allow plugins to send notifications + * plugins: Add simple pomodoro timer. + This follow the pomodor technique + https://en.wikipedia.org/wiki/Pomodoro_Technique although we don't + implement the longer breaks yet. + The `pomodoro-on.svg` is the `timer.svg` taken from + `org.gnome.design.IconLibrary`. `pomodoro-break.svg` is the + `tea-symbolic.svg` from the same library. + This also adds an example on how to use notifications from plugins. + * pomodoro-quick-setting: Add preferences dialog. + This also gives us a demo of preferences for a quick setting plugin. + * pomodoro-quick-setting: Mark more strings as translatable. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1127 + Thanks to Rafael Fontenelle + * notify-manager: Try harder to find an application icon. + Use `phosh_get_desktop_app_info_for_app_id()` so we get the correct icon + for e.g. Firefox which sends `Firefox` as `desktop-id` although the + desktop file is named `firefox.desktop`. + * screenshot-manager: Use a const rectangle. This eases bindings + * screenshot-manager: Use correct notification. If we don't copy to + clipboard we should say so, we also shouldn't claim we took a screenshot + when it failed. + * portals: Sort alphabetically + * portals: End list with semicolon + * portals: Use phosh specific portals where necessary. + The GTK portal doesn't support accent colors yet: + https://github.com/flatpak/xdg-desktop-portal-gtk/issues/498 + * portals: Disable currently unimplemented ones. + Let's not fall back to a portal implementation that might not work. + * quick-settings: Close phosh's settings when opening the Settings application. + Otherwise the user might be puzzled as seemingly nothing happens. We do + that by deduplicating the "open settings panel" logic which was similar + to what PhoshSettings does. Instead just use the action. + * quick-setting: Improve focus styling. + Use the selected bg color when focused. In order to keep the focus + visible on active widgets we use alpha blended foreground color there. + * sliders: Use selected color for outline when focused + * lockscreen: Use @theme_selected_bg_color for focus. This matches our + other list boxes + * top-panel: Drop `can-focus` properties at their defaults + * top-panel: Drop superfluous `receives-default` on a label + * top-panel: Place initial focus on arrow. + This avoids one button in the top panel having a focus border while the + other one hasn't. We can still keyboard navigate as the next focused + widget is still the lock button. + * quick-setting: Don't focus on click. This avoids the focus border for + quick setting but still allows for keyboard navigation. + * audio-settings: Don't take focus when showing details + * settings: Don't take focus when clearing all notifications + * bt-status: Page don't focus buttons on click. + The focus draws attention to the button while it should be on the + content of the status page. + * wifi-status-page: Don't focus buttons on click. + The focus draws attention to the button while it should be + on the content of the status page. + * shell: Use `once` function for timeout handling. Simpler and less error + prone + * testlib: Use `once` functions for timeout handling. While at that use a + more descriptive variable name + * tests/take-screenshots: Run wait in screenshot context. + This makes sure we don't discard the context before waits have + finished. + * ci: Update ci image + * ci: Update shared scripts to 5e3667e + * ci: Use common job for style, dist and markdown check + * ci: Reconfigure test job with coverage information. + The coverage json sometimes ends up completely empty. Make sure we built + with coverage support. + * ci: Use --gcov-ignore-errors=no_working_dir_found with gcovr + * lockscreen: Disable RTL for the deck. + Otherwise swipe and movement direction do not match + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1132 + * screenshot-manager: Use once timer function + * screenshot-manager: Use automatic cleanup + * screenshot-manager: Allow to save internal screenshots. + We separate the file name handling and decision whether we want to copy + to the clip board out of `phosh_screenshot_manager_do_screenshot` and + rather have callers decide. + In the case of internal screenshots triggered by keyboard shortcut or + power button menu we save to disk and copy to clipboard. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/964 + * screenshot-manager: Update recent file list. + Add screenshots to recent file list when saving screenshots not taken + via the DBus API. + * caffeine-quick-setting: Use a context for on/off. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1143 + * timestamp-label: Give translators more headroom. + We're not that tight on space here. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1144 + * notification-banner: Drop explicit notify. + No need to handle that for a construct-only property. While at + that modernize the doc string and indent. + * notification-banner: Adjust margin based on notification height. + Make sure the banner is initially slightly visible so the frame clock + ticks. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/221 + * notification-banner: Drop unused hard coded values + + [ Arun Mani J ] + * util: Move `open_settings_panel` here. Move it here as it not + quick-settings specific. + * quick-setting: Bind separately for `active` Have a new field to track + active binding instead of shadowing label binding. + * quick-setting: Move to new API. + Add support for status-pages. Previously, status-pages were separate + from the quick-settings. But now we make them a property of the widget + itself. + Previously, the status-page displaying was managed externally by + catching signals like long-pressed. But now we change the structure of + the quick-setting to make it show arrow icons at the right. Clicking + this arrow will emit the proper signal to show or hide status-page. + We remove read-only property `status-icon` as it seems unused. + We assign the widget a proper CSS name and reflect its `active` property + through CSS state-flag. + Since status-page source files are direct dependency of quick-setting + now, we move the `status-page.c/h` and `status-page-placeholder.c/h` to + `libphosh_tool` lists in `meson.build`. + * quick-setting: Clean up and format. Fix indentation, reformat code as per + `HACKING.md` and remove some unnecessary comments. + * plugins: Move to new quick-setting API. + Previously, `status-icon.h` was part of `quick-setting.h`, so plugins + were able to use it directly. But since we removed the `status-icon` + property from quick-setting, the corresponding header is also gone. + So add `status-icon.h` to all the quick-setting plugins. + * quick-settings-box: Add PhoshQuickSettingsBox + * tools: Add PhoshQuickSettingsBoxStandalone + * status-page: Simplify status page. + As `PhoshQuickSettingsBox` uses a revealer to display the pages, we no + longer need to have the status-page inherit from `GtkRevealer`. + With `PhoshQuickSetting` showing arrow to hide and reveal the status + pages, having a go-back button is redundant. + Also remove some empty lines from UI file as it looks odd with them. + * quick-settings: Add PhoshQuickSettings + * settings: Bind directly to torch-manager. + Drops dependency on torchinfo. Will be helpful when migrating to `PhoshQuickSettings`. + * bt-status-page: Use callback instead of action. + Makes bt-status-page more self-contained and helps when we simplify `PhoshSettings`. + * settings: Port to PhoshQuickSettings + * settings: Clean up and format. + Remove unused headers, actions and format using Uncrustify. + * wifi-status-page: Change switch to scan. + With the new quick-setting, one can toggle Wi-Fi by just clicking on + the quick-setting itself. So, let's use that space to have a button to + scan Wi-Fi access points. + * wifi-status-page: Remove "Scan" placeholder button. + Redundant with the new Wi-Fi scan button at top right. + * wifi-status-page: Remove unnecessary box. + The box as parent to stack is unnecessary. Instead make the stack a + direct child of status-page. + * custom-quick-settings-standalone: Use PhoshQuickSettingsBox + * tree: Export symbols of status-page and placeholder. + Allows plugins to use these symbols. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1090 + * simple-custom-quick-setting: Add status-page. + Add a status-page to demonstrate its usage in custom quick-settings. + * quick-settings-box: Set CSS name. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1125 + * quick-settings-box: Add bottom margin to status-pages. + Use bottom margin of status-pages instead of adding separate spacing for + revealer. This way there is no flickering (sudden disappearance of space). + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1124 + * quick-setting: Bind long-press to primary button. + As `GtkBox` does not have a `GdkWindow` on its own, it did not + receive the long-press gestures. So bind the gesture to primary button. + Also add the `long_press` object to C side to keep it alive. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1130 + * quick-setting: Handle `clicked` from button separately. + Otherwise, there is a conflict between `button-press-event` and + `long-press`, causing the quick-setting to emit both `clicked` and `long-press`. + * quick-setting: Use GtkGestureMultiPress for right-clicks + * quick-settings-box: Use most used type for variables. + Avoids superfluous type-casting. + * quick-setting: Use most used type for variables. + Avoids hitting line length limits and easy to read. + * quick-setting: Add long-press-action-{name,target} + As many quick-settings launch an action on long-press, make these as + properties, to handle this common usecase. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1133 + * quick-settings: Port to `long-press-action-*` properties + * quick-settings-box: Hide status-page when child is insensitive + * quick-setting: Unset state flags properly. + The previous method is both wrong and left the widget in a unstable + state. + * quick-settings-box: Fix typo + * quick-setting: Put property flags in a single line + * quick-settings-box: Put property flags in a single line + + [ Eugenio Paolantonio (g7) ] + * style-manager: Support accent colours. + React on accent colour changes via org.gnome.desktop.interface's + accent-color key. + When an accent colour is defined, @theme_selected_bg_color and + @theme_selected_fg_color in the stylesheet are overridden by + phosh with the actual colour. + * build: Bump gsettings-desktop-schemas dep to >= 47. + Required by the accent colours support. + * stylesheet: Fix accent colour on levelbars and calendar widgets. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1134 + + [ Sam Day ] + * screenshot-manager: Add nullable annotations to do_screenshot params + + [ Kenny Levinsen ] + * phosh.service: Manually issue chvt on start + libseat 0.9.0 no longer performs an automatic VT switch when taking + control of a seat to allow sessions to be started in the background. As + consequence, one must manually switch if one wants a background session + to be immediately activated. + Issue a chvt on ExecStartPost, when the service is deemed running. + + [ Anna (cybertailor) Vyalkova ] + * build: Split linter tests to their own suite. + Allow to easily opt out of code style tests, which provide little to no + value for end users. + + [ Teemu Ikonen ] + * lockscreen: Add 'require-unlock' setting. + Allows disabling lockscreen authentication by setting it to false. + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Martin ] + * Update Slovenian translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Artur S0 ] + * Update Russian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Andi Chandler ] + * Update British English translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Vincent Chatelain ] + * Update French translation + + -- Guido Günther Mon, 11 Nov 2024 12:07:45 +0100 + +phosh (0.42.0) experimental; urgency=medium + + [ Guido Günther ] + * subprojects: Switch to libcall-ui 0.1.4. + This allows us to drop the patch we apply to the subproject. + * panels: Queue draw instead of committing the surface. + If we commit too early and GTK hasn't updated the buffer the compositor + will terminate us due to the buffer size not matching the scale. + A draw will eventually commit the surface. This makes sure we don't + commit without giving GTK a chance to update e.g. the buffer size. + This is not ideal as we invalidate the complete surface causing a full + redraw. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1050 + * notify-manager: Hold a ref on the activated lockscreen notification. + Otherwise it might get disposed before we could action on it and we need + it around to emit the signal in `invoke_action`. + Tested-by: Teemu Ikonen + * build: Version the library name instead of the soname. + Now with a real consumer (phrog) let's rather version the API and use a + fixed soname. This unconfuses some packaging tools and also avoids file + conflicts between different API versions. + * packaging: Build bindings lib by default. + This ensures the build is tested. In contrast to any distro builds we + keep a fixed package name as the packages are meant for testing purposes + only anyway. + * overview: Ignore page changes when overview is not open. + Otherwise we might end up focusing the wrong app when another + adds a new toplevel on startup (like e.g. calls). + * wwan-mm: Don't warn when operation got cancelled + * shell: Avoid critical when there's no builtin monitor. + There might not always be a builtin monitor so check for that when + handling primary monitor changes. + * data: Drop feedbackd override. + Feedbackd sets this as default now. + * status-page-placeholder: Center align horizontally. + Otherwise the empty state ends up left aligned when there's + horizontal space. + * packaging: Let changelog hook handle rc versions. + This allows our release pre script to work for RCs and actual releases. + + [ Gotam Gorabh ] + * notification-content: Set action-area border to none. + Remove the line that is under the notification frame. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1027 + * notification-frame: Add separator to separate notification content. + Fix the visual separation between notifications from the same app. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/901 + * notification-content: Add background color to action-area button. + Improve visual separation between notification body and actions, this + allows us to drop the border which would otherwise look odd with the + added separator. + * locksreen: Add border color to the notification separator. + Use phosh-borders-color for the notification separator otherwise it has + the same color as the notification background. + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + -- Guido Günther Sat, 28 Sep 2024 19:23:48 +0200 + +phosh (0.42~rc1) experimental; urgency=medium + + [ Guido Günther ] + * build: Specify minimum versions for plugin pref libs. + We're requiring a minimum libadwaita version to not hit deprecations so + we should specify that as version requirement. While at that do the same + for GTK4. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1104 + Tested-by: Anna Vyalkova + * wwan-mm: Handle access-tec bitmasks better. + The switch statement missed things like + MM_MODEM_ACCESS_TECHNOLOGY_GPRS | MM_MODEM_ACCESS_TECHNOLOGY_GSM + so instead of adding all possible combinations to the switch use if / + else instead and check from newest generation to oldest ones. + This makes the mobile network generation show in more cases. + * lockscreen: Improve documentation. + This shows up in the + * shell: Document ready signal + * wall-clock: Fix parameter names in declarations. + Make them match the ones used in the definition + * wall-clock: Document class and properties. + This makes the Rust docs look more complete + * lockscreen: Improve enum doc. + Use backticks to they work better in markdown and separate summary from + the details. + * docs: Add phosh-config.h to example + * build: Disable feedbackd tests. + They just take time and the test options confuse ASAN + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1108 + Tested-by: Arun Mani J + * wifi-hotspot-quick-setting: Make sure 'state' signal gets disconnected. + The WifiManager outlives the quick setting (which might get added / + removed at runtime when enabling disabling quick settings) we hence need + to make sure we disconnect the signal when the quick setting goes away. + Do so by using `g_signal_connect_object`. + This fixes that looks like + #0 0x00005652da08fc4f in PHOSH_IS_STATUS_ICON (ptr=0x21) at ../src/status-icon.h:15 + #1 phosh_status_icon_set_info (self=0x21, info=0x7f5429c09061 "Hotspot Off") at ../src/status-icon.c:377 + #2 0x00007f5429c0851f in update_info (self=0x565300804880) at ../plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 + #7 0x00007f542c0fffc3 in + * wifi-hotspot-quick-settings: Use `_cb` suffix. + If we don't use the `on_` pattern because we want to describe what + happens rather then on what event the callback is triggered we append + `_cb` to indicate a callback. + * shell: Make get_locked() available to plugins. + Useful to determine their lockscreen state + * wifi-hotspot-quick-setting: Disable on lock screen. + We don't allow changing Wi-Fi networks so we shouldn't allow enabling + hotspot either. + * treewide: Use margin-{start,end} instead of margin-{left,right} + The later are deprecated + * tools: Add check so that deprecated tags don't creep back in + * packaging: Add dependency on desktop schemas. + They're a dependency of the pkg-config file so we need to add them here + too. + * docs: Drop superfluous 0 initializer + * docs: Clarify callback naming. + Add more details on how we name the different handlers. + * build: Bump polkit dependency. + The needed versions are in Debian stable. This allows us to drop some + `ifdef`s. + * wwan-iface: Fix base class. + Implementers must derive from PhoshWwanManager. Both the MM and Ofono + implementation do this, this just marked it explicit. + * wwan-mm: Simplify modem cold plug. + This ensures we do the same things as in the hotplug path. + * wwan-mm: Disconnect by data. + Easier than storing the ids + * build: Depend on mm-glib. + Used for cell broadcast and will be used for more modem integration + * wwan-mm: Use MMManger instead of our generated object manager. + We'll use more things from mm-glib so use this to interface with + ModemManager. + This also allows us to drop enums copied over from mm-glib + * wwan-mm: Use our generic dbus_client_protos. + Since we dropped the object manager there's no need to open code it + anymore. + * wwan-mm: Switch to MM proxies for modem and modem3gpp. + This allows us to drop the gdbus-codegen generated bits. + Add cancellable while at that. + * wwan-mm: Move manager object creation to init. + No need to postpone it until constructed + * wwan-mm: Simplify modem destroy. + We want to guard the signal removal in case the modem didn't acquire the + proxy but there's no need to guard the `g_clear_object`. + * wwan-mm: Rename proxy to proxy_modem. + We have a bunch of DBus proxies to distinguish so add a suffix here too. + * wwan-mm: Disconnect signal handlers by data. + We want to disconnect all signal handlers for the proxy so no need to + track them by id. + * dispose: No need to clear data twice. + We free all the modem bits in phosh_wwan_mm_destroy_modem + and invoke that from dispose so no need to do it in dispose + explicitly too. + * docs: Cleanup markdown + * ci: Fix indent. + Let's make yamllint happy + * ci: Lint markdown. + Lint markdown files when they change in a merge request + * ci: Drop `except:`s. + They're not used much nowadays and complicate the logic + * ci: Drop submodule updates. + Not needed as we use meson subprojects + * ci: Build library. + Make sure this works as expected + * ci: Verify libphosh install. + Compile a small stub to ensure the pkgconf file works + * upcoming-events: Add missing plugin prefs variable. + Otherwise the `Plugin=` path is broken in the generated .plugin file + Fixes e1a1ae1a8 ("upcoming-events: Add preferences widget") + * libphosh: Drop generated header. + This got removed in + db3925449 ("wwan-mm: Switch to MM proxies for modem and modem3gpp") + * ci: Use gmobile-dev on alpine. + This avoids the subproject clone which fails ever so often + * treewide: Drop `v` from since versions. + This trips up `GirXml` as it inserts `doc-version`s into the generated + Gir. + * wwan: Modernize property docs. + There was a typo so while at that clean up all of them + * wwan: Add getter for has-data property. + While at that we group the interface methods and the ones + we proxy from WWanManager. + * mobile-data-quick-setting: Make insensitive when modem is disabled. + It's confusing when it stays enabled as toggling it would do nothing + when the modem is disabled. + * symbols: Export phosh_status_icon_get_type. + We use the type so should allow users to check it. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1115 + * dbus: Add Backlight property to DisplayConfig. + This ensures GNOME 47's gsd-power can read it and doesn't crash. + Having it empty is fine for the moment. + * build: Use schema headers var. + No need to track it in two places + Fixes bdcb925c6 ("enums: Introduce single header file to gather all enums") + * build: Ensure the enum header end up in the docs. + Fixes bdcb925c6 ("enums: Introduce single header file to gather all enums") + * overview: Simplify activity creation a bit + * protocols: Update wlr-foreign-toplevel-management + * toplevel-manager: Track parents. + For that we require version 3 of the protocol which is available in + all phoc's that use wlroot 0.15.0 or later. + * activity: Update app-id if an activity's toplevel has a parent. + The app-id is often incorrect on dialogs so use the parent's + app-id in that case. + * lockscreen: Swap horizontal deck and vertical carousel. + This avoids accidentally switching to the plugins widget when entering + the pin without complicating the logic further. + This is the minimal UI reshuffle. We'll reindent in a separate commit. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/899 + * lockscreen: Vexpand the deck. + Otherwise we might not take enough vertical space and parts of the + keypad screen leak into the info box (e.g. with 1920x1080). + * libcall-ui: Drop CuiCalldisplay margins. + This allows us to work on smaller displays, see + https://gitlab.gnome.org/World/Phosh/libcall-ui/-/merge_requests/92 + Helps: https://gitlab.gnome.org/World/Phosh/phoc/-/issues/371 + * notify-manager: Handle per app 'enable' toggle. + If we get a notification for an app that has notifications enabled + ignore it. This ensures that new notifications for apps that have them + disabled don't end up in any notification sources at all. + Note that we still allow existing notifications to be updated by the app. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/168 + + [ Jared Toomey ] + * notification-feedback: Add feedback responses for new call categories. + See https://gitlab.freedesktop.org/xdg/xdg-specs/-/merge_requests/50 + + [ Evangelos Ribeiro Tzaras ] + * doc: Fix URL in configuration. And add a newline while we're at it. + + [ Arun Mani J ] + * wifi-manager: Ensure Wi-Fi network stays alive. + Ensure Wi-Fi network stays alive for the duration of callback. This + prevents cases where the callback is called for a destroyed network object. + * wifi-manager: Clean up Wi-Fi network from signal after usage. + We need to watch the connection only till it crosses the "activating" + state. So disconnect once it happens to cross it. + Then we unref the connection that was made available at the + `wifi_connection_*_activated` functions. + * wifi-manager: Remove unused header + * upcoming-events: Set stack page on setting model. + By default, the stack is on "no-events" page. We want to set the + appropriate page whenever one assigns a new model. + * upcoming-events: Allow day-offset up to maximum integer + * upcoming-events: Change day-offset to unsigned int + * upcoming-events: Allow configuring number of days. + Instead of showing a fixed number of 7 days, users can now set the + number of days shown to any positive integer. + * upcoming-events: Add preferences widget + * upcoming-events: Remove unnecessary empty line + * upcoming-events: Remove unused header + * upcoming-events: Add XML header to schema + * upcoming-events: Fix a few typos. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/970 + * torch-manager: Add `can-scale` property. + This property indicates if the torch brightness can be scaled. + * settings: Show revealer only if torch can scale + * torch-manager: Move to modern property description. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/858 + + [ Sam Day ] + * shell: Register plugin extension points in constructor. + This makes it possible for PhoshShell subclasses to register lockscreen + and quick-settings custom widgets. + It introduces an implicit dependency that extension points will not be + looked up until the shell setup_idle callback. + * shell: Minor formatting fix in phosh_shell_init + * shell: Move cui init/de-init to Shell object lifecycle. + This makes sure that embedding contexts don't need to concern themselves + directly with linking to and initializing libcall-ui. + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Bruce Cowan ] + * Update British English translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Artur S0 ] + * Update Russian translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Martin ] + * Update Slovenian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Vasil Pupkin ] + * Update Belarusian translation + + [ Andi Chandler ] + * Update British English translation + + -- Guido Günther Fri, 20 Sep 2024 17:26:38 +0200 + +phosh (0.41.0) experimental; urgency=medium + + [ Guido Günther ] + * media-player: Use G_GINT64_FORMAT to print gint64. + Otherwise we'll break i386 + * lockscreen: Delay clock update past the animation. + The visible child changes at the start of the animation hence the clock + can still be outdated. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/922 + * plugins: Avoid adw deprecation warning + + [ Sam Day ] + * lockscreen: Rename PHOSH_LOCKSCREEN_PAGE_INFO. + This better describes the purpose of this page. Further, it lays + groundwork to introduce functionality that changes the default page + displayed on the lockscreen. + * lockscreen: Clean up some unused imports + * lockscreen: Always clear idle timer in carousel_position_notified_cb. + Even if the keypad is already visible, any swiping activity triggers + carousel_page_changed_cb again anyway. + This actually slightly improves UX in a somewhat edge case condition. If + you swipe to the keypad, and then swipe up/down in the keypad region + without any keypad input (or switching all the way back to the page + above), the idle timer will kick in and suddenly take control of the + carousel. With this change, *any* swiping behaviour properly resets the + idle timer. + This is fortunate, because the intent is to remove "PhoshLocksreenPos" + imminently, and this change removes two usages of that enum. + * lockscreen: Use phosh_lockscreen_set_page more. + The logic in phosh_lockscreen_set_page was duplicated in show_info_page + and show_unlock_page. + Since show_info_page wasn't doing anything else, just remove it entirely + and use phosh_lockscreen_set_page. + In the case of show_unlock_page, it's also emitting a signal, and is + used as a callback from the UI XML, so it remains intact. + * lockscreen: Remove PhoshLocksreenPos enum. + Instead, phosh_lockscreen_get_page is used. The logic of this function + was also tweaked a bit to incorporate the rounding that was previously + only done in key_press_event_cb. + This means that phosh_lockscreen_get_page will report the current page + slightly differently if queried while the carousel is animating or being + dragged. + * lockscreen: Support setting the default page. + The default page will be presented when the lockscreen is shown, it is + also the page that is scrolled to when keypad expires or the escape key + is pressed. + * lockscreen: Allow custom unlock submit callback. + The callback that is fired when a passphrase is submitted from the + keypad entry is virtualized so that subclasses can override this + behaviour. + This change primarily benefits Phosh-based greeters like Phrog. In this + case, the login behaviour is entirely different from the default (the + PAM conversation is mediated through greetd IPC, and takes place before + there's actually any user logged in). + * lockscreen: Provide methods to get/clear/shake PIN entry. + This is useful for lockscreen subclasses that want to override or + influence the behaviour of the authentication flow. + The shake_label/finish_shake_label functions were renamed, since they + shake the entry, not the label. + * lockscreen: Allow an "extra" page to be inserted. + If phosh_lockscreen_add_extra_page is called, the carousel will have an + extra page inserted between the info page and the unlock page. This + extra page can then be navigated via swipes, and also set as the + current and/or default page. + This is useful for the Phosh-based greeter use case (for example Phrog), + which inserts a user/session selection page. + * lockscreen: Setter for unlock status label. + Allows subclasses to override the message displayed above the keypad + entry. + * lockscreen: Property for current carousel page. + This property can be used by subclasses / observers to be notified when + lockscreen page considered "visible" changes. + * lockscreen: Don't set label after shaking. + The existing usage is changed to set the label before shaking. + This way the shaking mechanism can be used in other scenarios, + particularly by lockscreen subclasses. + * lockscreen-manager: Add phosh_lockscreen_manager_get_lockscreen + * tests/lockscreen: Add test for extra page functionality + * background: Use G_GNUC_FALLTHROUGH for consistency + * bt-manager: Replace #include with a typedef. + This header is vendored into phosh because upstream does not want to + make it public, so we can't leak it into libphosh headers. + Further usage of types from these vendored headers are not anticipated + for now, so we can get away with a typedef for the one BluetoothDevice + type we need. + * libphosh: Include status-page-placeholder.h. + PhoshStatusPagePlaceholder has bled into other publicly exported types + from libphosh, so we need to make sure this is included. Elsewise + libphosh-rs compile-time ABI tests fail, for example. + + [ Arun Mani J ] + * subprojects: Sync libfeedback version with packaging. + Let's use the same version everywhere + + [ Anders Jonsson ] + * Update Swedish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Scrambled 777 ] + * Update Hindi translation + + -- Guido Günther Thu, 15 Aug 2024 10:17:28 +0200 + +phosh (0.41.0~rc1) experimental; urgency=medium + + [ Teemu Ikonen ] + * media-player: Add a label with track position and length. + The 'Position' property is read every 1 s when the player is playing. + * media-player: Add a ProgressBar below details button + * media-player: Show box_pos_len only when MPRIS has Position and length + + [ Guido Günther ] + * packaging: No need to set tests twice + * subprojects: Use a wrap for libgnome-volume-control + * subprojects: Update gvc. + Update to 5f9768a2eac29c1ed56f1fbb449a77a3523683b6 to fix a crash + with BT speakers + * submodule: Use wrap for libcall-ui. + This makes all subprojects consistent + * subrojects: Update libcall-ui to 0.1.2. + Pull in the build system improvements to avoid build warnings. + * auth: Make async function names match the async/finish pattern. + * audio-devices: Use gpointer instead of GvcMixerControl. + * head: Prefix struct name. + * build: Make gir warnings fatal + * ci: Avoid check-doc. + It trips onto all kinds of things and fail on both introspection and + gi-docgen warnings no. + * wifi-status-page: Mark more strings as translatable + * settings: Mark "Notifications" as translatable + * prompt: Only show when we got a request. + So far we showed the prompt right away which led to some flicker when + the UI elements were made visible and can also lead to an empty dialog + when non of system_prompt_{password,confirm}_async are ever invoked. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1092 + * docs: Mention `gtk-builder-tool --simplify` + * style: Drop unused osk-button class + * wifi-status-page: Wrap text. + This can easily overflow in translations + * ci: Ensure build-deps are present when building dist tarball. + Meson checks for available libraries so we need those. + * check-license-header: Fail on too short header. + We expect it to at least have one prelude, then copyright. + * bt-info: Use g_connect_object. + Less code. + * bt-info: Only connect to enabled once + * settings: Use an action to toggle Bluetooth. + This allows us to use the same code on status page and quick setting + and makes it available to plugins. + * settings: Only apply margin to toplevel viewport. + The current CSS would apply a wide margin to all viewports in settings + but we only want it for the toplevel one that contains all the widgets + but not for others like the ones in status pages. + * settings: Use background color for all separators in settings. + Otherwise they're hard to see. + * status-page: Remove phosh-settings-list-box style class. + This breaks the HdyPreferencesRow styles and we only want to use it for + the sound list boxes not the status pages in quick settings. + * bluetooth: Depend on gnome-bluetooth. + We need to add the headers as they're currently not shipped upstream. + We might bundle the library instead. + This is from cccaaa7928c2b9195295d8c7dae80665b4b62c4d + * bt-manager: Track connectable Bluetooth devices. + We track connectable Bluetooth devices and add some properties based on + that like the number of currently connected devices. + * bt-info: Improve available information. + If at least two devices are connected we print the number of connected + devices. If a single device is connected we print it's info. + * status-page: Add placeholder / empty state widget. + Add a placeholder widget for the displaying the empty/disabled state in + settings status pages. This is very similar to `HdyStatusPage` but we + add a custom widget as we can't scale down the icons in GTK3 via CSS as + -gtk-icon-size doesn't exist yet and -gtk-icon-scale scales the icon but + doesn't shring the size allocations. + This also helps to ease displaying empty states for list boxes as the + `placeholder` there doesn't work well with advanced styling. See + libhandy#468. + This can be replaced with AdwStatusPage once we switch to GTK4. + * status-page: Introduce page header bar and a footer. + This avoids leaking parent widget details into the derived classes (like + requiring a `packaging` `end`). + The optional footer usually holds a button to open Settings. We use a + `GtkBox` to place a single child. We'd use `GtkBin` but that is + abstract, in GTK4 we'll use `AdwBin`. + The header contains the back button and has a user defined title. One + also get a user defined child widget. + * wifi-status-page: Use page header. + This uses the page header introduced in the previous commit. + * wifi-status-page: Use StatusPagePlaceholder. + We also remove the superfluous position packing. + * wifi-status-page: Use wireless-disabled symbolic for disabled state. + It's the icon meant for this state + * settings: Add a Bluetooth status page. + The status page lists the currently connected devices in a list box and + allows to connect / disconnect them by activating the list box row. + * bt-status-page: Hide enable button when there aren't any devices. + While at that streamline the logic a bit by avoiding putting the `TRUE` + branch first and using minimum scope for variables. + * settings: Fix id of Bluetooth status page. + Fixes: 5807fa273 ("settings: Add a Bluetooth status page") + * settings: Return `NULL` when we return a pointer + * settings: Drop nested stack. + No need to have a separate stack for status pages. This just adds + more size calculations when GTK lays out the widgets. + * settings: Use g_timeout_add_once + * status-page: Make a revealer. + This allows us to use more vertical space + * settings: Add handler for quick setting stack page changes. + We will add more logic here soon. + * settings: Do not reveal status pages when not shown. + Don't reveal the status pages when the quick settings are + visible. This ensures they don't take any vertical space ensuring + we don't push down other elements in settings (like the media player + or notifications) when there's e.g. a lot of Wi-Fi networks. + We reveal the children individually as they might have different + heights so we move down the other UI elements as little as possible + when a status page is shown. + * status-pages: Remove scrolled windows. + They're not needed as the whole settings panel is scrollable + and interact badly with the revealer. + We could swap revealer and scrolled window but that's actually not + necessary. + * status-pages: Reindent ui files. + Do this in a separate commit to ease review + * settings: Use less top margin for notification empty state. + This avoids the scroll bar even when we add more quick settings. To keep + the empty state centered with fewer quick settings we valign centered. + * settings: Move top margin from quick settings to the stack. + This ensures the status page's top aligns with the top of the quick + setting and we get a bit more "visual distance" between the sound + slider and the status page. + * feedback-manager: Ack libfeeback unstable API in meson. + This avoids cluttering the code with multiple defines + * main: Init libfeedback early. + This moves all library inits to single place and avoids a + end_notify_feedback: assertion 'lfb_is_initted ()' failed + warning on startup. We can ignore all error handling as the feedback + manager will try reinitialization later on anyway. + * keyboard-events: Drop fallback for ancient phoc + * feedback-manager: Allow to set profile + * keyboard-events: Emit signal on keybinding press. + Emit a signal when a subscribed keybinding is pressed. This allows other + parts of the shell to perform additional activities on global + keybindings (that possibly aren't even subscribed by Phosh itself). + * calls-manager: Track incoming state. + This allows to check if the active call is in incoming state. + As with the rest of calls manager this will need some tweaking once + calls handles multiple active calls. + * build: Bump feedbackd dependency. + We want 0.4.0 as only this does end running events on profile changes. + * shell: Allow to silence the device on incoming calls. + When the shell is locked and there's an incoming call pressing the + button to lower the volume will put the device into silent mode. + Bump copyright while at that as this didn't happen since some time. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/780 + * app-tracker: Log success at `debug` priority. + Fixes: 1226ae949 ("app-tracker: Move launched processes to transient systemd scope") + * screen-saver-manager: Send locked hint. + * settings/brightness: Fix indent + * settings/brightness: Make globals static. + These are only meant for the current compilation module + + [ Arun Mani J ] + * wifi-manager: Decouple hotspot check from icon. + Since b3c8142a ("wifimanager: Track whether we're an active wifi + hotspot") `update_icon` checked the value of `is-hotspot-master` as a side + effect. This leads to the state not being updated when there was no + active connection as in this case the check is never run. + Fix this by moving the hotspot check before any icon updates making + `update_icon` side effect free again. + * wifi-manager: Clear SSID when connection is cleaned. + We clean the `ssid` when the AP changes but we also need to clear it + when our network device tracking the connection goes away. + * wifi-network-row: Port to HdyActionRow + * wifi-status-page: Add content style class + * wifi-status-page: Set scrolling related properties + * wifi-status-page: Merge placeholders to one + * wifi-network-row: Simplify UI via gtk-builder-tool + * wifi-status-page: Clean up code and simplify UI + - Move headers to `wifi-status-page.c`. + - Fix formatting in `wifi-status-page.c`. + - Simplify UI via `gtk-builder-tool`. + * mobile-data-quick-setting: Drop unused variables + * wifi-manager: Rename connection callbacks. + We rename connection activated and added-activated methods to prevent + conflict with similar methods for hotspot. + * wifi-manager: Extract hotspot check method. + This will be used in further commits, so extract it out to avoid duplication. + * wifi-manager: Add method to toggle hotspot + * wifi-manager: Create hotspot connection if one doesn't exist + * wifi-manager: Rename update methods + * wifi-manager: Fix typo + * wifi-manager: Add property `state` + * wifi-status-page: Add turn off hotspot button + * plugin-shell: Export Wi-Fi manager + * build: Export Wi-Fi manager symbols + * plugins: Add Wi-Fi hotspot quick setting + + [ Evangelos Ribeiro Tzaras ] + * run: Export XDG_CURRENT_DESKTOP. + Otherwise .desktop files with OnlyShowIn= might end up ignored. + * keypad: Force left-to-right button layout. + Real life keypads are the same for in RTL and LTR locales. + Closes https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1086 + Modelled after similar change in libcall-ui + https://gitlab.gnome.org/World/Phosh/libcall-ui/-/merge_requests/89 + * libcall-ui: Update to 0.1.3. + Forces LTR text direction for keypad in the call display + * polkit-auth-agent: Init autocleaned variable to NULL. + While it isn't technically necessary here because of the assignment + that always takes place a couple lines below, it deviates from the pattern + established throughout the codebase and requires closer inspection + increasing mental load. + * polkit-auth-agent: Register asynchronously + + [ Cédric Bellegarde ] + * settings: Block value changed handler id. + When brightness is updated by gnome-settings-daemon DBus API, ignore value-changed signal. + Otherwise, we will send value back to GSD. + Fix #1093 + + [ Xiao Pan ] + * docs: Update URL to GTK inspector documentation + + [ Daniel Rusek ] + * Update Czech translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Antonio Marin ] + * Update Romanian translation + + [ Martin ] + * Update Slovenian translation + + [ Artur S0 ] + * Update Russian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Scrambled 777 ] + * Update Hindi translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + -- Guido Günther Thu, 08 Aug 2024 10:58:30 +0200 + +phosh (0.40.0) experimental; urgency=medium + + [ Guido Günther ] + * build: Install headers and shared lib for Rust binding generation. + This allows to install headers, pkgconfig file and shared library to + create Rust bindings. + We create a unified header pulling in (almost) all headers so they can + be consumed by just including libphosh.h freeing binding generators from + having to keep up with added/removed headers. + As installing these files is used for binding generation only atm we + make it depend on a new 'binding-lib' meson options. + These files are not (yet) meant as public interface so if using these + make sure to bundle libphosh with your project. + * ci: Trigger libphosh-rs build. + We only do this on commits to RUST_BINDINGS_BRANCH (default main) and if + src/ changes. + The branch triggered downstream is also RUST_BINDINGS_BRANCH. This makes + it simple to work on pipeline changes. + * resources: Sort ui files alphabetically + * wwan-mm: Add present and enabled properties to get_property. + Fixes 1a75bcb48 ("wwan-mm: Handle 'enabled' property") + Fixes 3a0e8eab6 ("wwan: Report modem presence") + * wwan-mm: Modernize property change emission. + Use the faster by_pspec variant. Shorten property names while at that. + * settings: Drop superfluous wifi_status_page + * settings: Use property binding for wifi's has-status + * settings: Use explicit notify for on-lockscreen. + This toggles multiple things so let's reduce the work done here. + * settings: Drop on_shell_locked. + We can do the work when setting the property. + * wifi-manager: Use automatic cleanup. + This way we don't need to cast away `const` either. + * wifi-manager: Drop len variable. + Using ap_cnx->len directly make it a tiny bit more obvious which + length were' looking at. + * main: Instantiate shell after the needed objects. + The shells object construction needs the Wayland object and the + background manager. + Fixes: 1bb5c8c63 ("shell: Allow shell singleton to be overridden") + * service: Adjust oom score. + This avoids the shell getting killed early in tight memory scenarios. + While at that tweak the restart behavior. + * session: Drop rootston.ini fallback. + This is from long gone times + * treewide: Drop support for --builtin session fallback. + Always use systemd as gnome 46's gnome-session doesn't support anything + else. Thanks dhjg2000 for figuring this out + * build: Remove thumbnail from tool sources. + We stub it so it should be in the classes not available to tools. + * tests: Move stubs into their own lib. + Preventing the rebuild of the stub sources for all tools and unit + tests brings down the number of targets to build by another 25% + (1184 before vs 885 now). + * tests: Test command line options. + We can test the options that exit early making sure they don't break + accidentally. + * wwan-manager: Split out wwan connection type comparison. + Will be used in a follow up commit + * wwan-manager: Add helper to (de)activate a data connection. + For that we add a helper to track the last active connection. This + allows us to check if there is a data connection available at all and + which one is the last active. + * data: Add mobile data icons. + Self sketeched and optimized with svgo. + * plugins: Add mobile data toggle. + This allows to toggle mobile data on/off. The plugin is marked inactive + if thre's no mobile data connection configured. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/691 + * data: Use mobi domain for override file + * data: Make all overrides phosh specific + * data: Unlock SIM by default + * data: Disable ambient light adjustments by default. + The current algorithm confuses people on phones. We want to tweak that + in g-s-d before enabling it by default. + * data: Let g-s-d do nothing on power button press. + Phosh handles it. + * data: Add Firefox ESR and TextEditor to adaptive apps. + We assume that FF uses mobileconfig and TextEditor adapts nicely + * data: Set default idle-delay to 60 seconds. + It's more suitable than the default 5 minutes. + * background-manager: Don't try to get background for zero size. + Otherwise we crash on gnome_bg_slide_show_get_slide on e.g. 0 width or + height. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1069 + * background: Avoid update when the layer surface isn't configured yet. + Otherwise we might feed bogus values to GnomeBg. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1069 + Tested-by: Boud Roukema + * power-menu-manager: Sort actions alphabetically + * power-menu-manager: Listen for locked instead of shell state changes. + This triggers less often. + * power-menu: Add a property to show a suspend button. + If `TRUE` the suspend button is used. Otherwise the `power off` button + is shown. + * power-menu-manager: Allow to suspend from power menu. + If suspend is allowed we show the suspend action rather then disabling + the power off button. + This makes the power button menu more useful on the lock screen. And + since it's a non-destructive action there's no issue with allowing it. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/990 + * top-panel: Don't show lock and menu button when locked. + They have no purpose and are hence thus confusing. + * screen-saver-manager: Half the long press timeout. + This should still be long enough to not trigger it accidentally. + * screen-saver-manager: Emit button-pressed feedback on long press. + This gives some haptic feedback when the even triggers + * screenshot-manager: Wire up screenshot keybinding. + Defaults to + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/931 + * shell-manager: Simplify version setting. + No need to override the r/o property. We just need to set it once. + * shell-manager: Implement OverviewActive property. + This implements another property from that interface and helps + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1068 + * main: Use G_NORETURN. Makes clang happy + * main: Use automatic cleanup for err + * tools/custom-quick-setting: Accept list of plugins to show. + This allows to skip plugins the e.g. need the shell singleton + * tools/custom-quick-setting: Use newline when printing plugin name + * tools/custom-quick-setting: Top align flowbox. + This avoids vexpanding the quick settings itself. + * plugins/dark-mode: Add and use dark-mode-disabled icon + * app-tracker: Always extract pid + * app-tracker: Add cancellable. + We do this with other DBus calls, so do the same here too. + * app-tracker: Move launched processes to transient systemd scope. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/963 + * launcher-item: Add has-data property. + This is TRUE when count or progress should be shown. It will be used to + toggle the visibility of the box displaying the data. + * launcher-row: Use width-request for status information. + With that we can align to the row's end as otherwise the items + look out of place. + In order to not truncate longer descriptions we only make the status + information box visible when needed. + * docs: Add note about DBus helpers in the tools/ directory + * tests: Add mock for ModemManager and NetworkManager. + This is helpful to test mobile data and other features on devices + without a modem + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1051 + * tests: Add checks for python helpers. + This will be reused when we use python based tests that run the mocks. + We use black and ruff or flake8 so devs have some leeway (e.g. ruff + isn't in Debian testing atm). + * doc: Mention NetworkManager and ModemManager DBus mock. + This way it's hopefully found. + * settings: Set default sound theme. + When installed this gives us improved sounds for incoming messages and + calls. + * plugins/dark-mode: Update icons. + The moon is overloaded and dark mode isn't only for the night. + This dark mode icon was suggested by Sam Hewitt. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1071 + * media-player: Don't dim unsensitive button. + Fixes: 042c0f464 ("Add media-player widget") which got broken by + d79a379d5 ("style: Unified the button styles") + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1073 + * libphosh: Drop wwan-backend.h. + Fixes: bdcb925c6 ("enums: Introduce single header file to gather all enums") + * build: Prefix library include dir. + Use a lib_ prefix to distinguish this form paths we use for headers used + by plugins and tools that supplement the actual shell. + * build: Add dependency on phosh-settings. + We want the header (and don't want to duplicate it) + * build: Set a soname for the shared library. + We use the project version underscored dropping micro revisions. + * build: Use a relative path to the enum headers. + * ci: Check 'meson dist' + We've been hitting tiny nits before release ever so often. Avoid these. + + [ Sam Day ] + * shell: Make PhoshShell a derivable type. + Since this type is meant to be consumed by downstream libphosh + consumers, a bunch of vtable space is reserved for future expansion. + * shell: Allow shell singleton to be overridden. + This follows the pattern first established with + phosh_wall_clock_set_default. + This allows libphosh consumers to override the default shell singleton + with a custom subclass. + * lockscreen: Make PhoshLockscreen a derivable type. + Since this type is meant to be consumed by downstream libphosh + consumers, a bunch of vtable space is reserved for future expansion. + * lockscreen: Support construction of custom type + phosh_lockscreen_new now requires a GType parameter that should be a + type that derives from PhoshLockscreen (or is PhoshLockscreen itself). + This functionality is not fully exposed yet. PhoshLockscreenManager is + just passing a hardcoded PHOSH_TYPE_LOCKSCREEN to phosh_lockscreen_new. + * shell: Allow lockscreen type to be customized. + A virtual function is added to PhoshShell to determine which lockscreen + type should be constructed. PhoshLockscreenManager consumes this vfunc + via phosh_shell_get_lockscreen_type. + This change makes it possible for libphosh consumers to use a custom + lockscreen, exposed through their custom shell type. + * ci: Add "startup test" job. + This job runs the main app entry point, and confirms that the shell + startup signal handler is reached. + + [ Artur S0 ] + * Update Russian translation + + [ Scrambled 777 ] + * Update Hindi translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Martin ] + * Update Slovenian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Teemu Ikonen ] + * plugins: Add dark mode toggle. + Toggles the "org.gnome.desktop.interface.color-scheme" enum between + 'default' and 'prefer-dark' with a quick setting button. + + [ Antonio Marin ] + * Update Romanian translation + + [ Arun Mani J ] + * app-grid-folder-button: Force 16px as icon size. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1072 + * app-grid-folder-button: Force pixel size of 24px. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1081 + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Gotam Gorabh ] + * enums: Introduce single header file to gather all enums. + The aim is to allow phosh-mobile-settings to use these to map GSettngs values. + This will bring all the enums into a single header file to tackle duplicacy as well. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1036 + + -- Guido Günther Mon, 24 Jun 2024 13:38:32 +0200 + +phosh (0.39.0) experimental; urgency=medium + + [ Guido Günther ] + * power-menu-manager: Remove unused settings variable. + Fixes: 988240d3a ("Add power-dialog and manager") + * build: Drop `lib` when looking for libsoup via find_library() + Silences a meson warning + * build: Sort dependencies alphabetically. + Sort by meson variable name. + * packaging: Allow to fetch meson wraps. + This is needed as long as gmobile isn't in Debian + * build: Allow to use shared gmobile + * upcoming-events: Adjust to gmobile API change + * build: Use wrap for gmobile. + We prefer the shared gmobile so no need to ship as submodule + * animation: Remove trailing whitespace + * shell-manager: Prefix action mode enum + * wifi-info: Fix property description + * folder-info: Fix property description. + We ought to end with a ':' + * util: Fix parameter name. + Let it match the doc string + * monitor-manager: Add docstrings. + This gets rid of the missing transfer annotation warnings + * notify-manager: Make defines private or prefix properly + * shell-manager: Prefix keybinding flags + * app-auth-prompt: Properly namespace CHOICES_FORMAT + * build: Drop contrib/shell-network-agent.h from list of sources. + This makes sure it isn't exposed to gir generation and we don't need + it's doc as it's in contrib/ anyway. + * build: Add gcr-3 to list of gir includes. + Helps to resolve GcrSystemPrompter + * plugin-loader: Add missing transfer annotations + * audio-device-row: Add missing transfer annotation + * system-prompter: Add transfer annotations + * treewide: Mark some functions as not introspectable. + They still show up in the docs. + * ci: Update shared files. + Use files from meta-phosh as of 240dfc8825640041f5b47a207e9339b9c6365a53 + * folder-info: Test locating and parsing .directory files too + * folder-info: Test app-info interface + * folder-info: Test app-infos property getter + * notifications: Don't glue timestamp label together. + Rather use full translatable strings that allow translators to e.g. + handle different plural forms. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/785 + * timestamp-label: Fix translator comment for hours. + Fixes: a09453126 ("notifications: Don't glue timestamp label together") + Thanks to Anders Jonsson + * util: Drop support for glib < 2.76. + We bumped the minimal requirement in + decc80c13 ("build: Bump glib to 2.76") + Glib 2.78 is in Debian stable. + * audio-device: Remove pulseaudio.h from header. + This trips up the gir scanner and isn't needed + * build: Acknowledge that gmobile is unstable API. + No need to do it in every source file using it. + * treewide: Use gm_str_is_null_or_empty + * util: Drop STR_IS_NULL_OR_EMPTY. + We have it in gmobile.h now + * check-doc: Tighten expression and modernize. + Get this in sync with phoc and reduce the number of allowed patterns + * data: Allow gnome-clocks to override the feedback level. + This allows alarms to trigger even in silent mode + * util: Test the uri_to_pixbuf for broken URLs too + * tests: Add some metadata to the mpris test. + This way we validate long titles and image URLs + * treewide: Drop config.h usage. + We dropped them a while back but they creep back in when polishing + ancient MRs. + Fixes: 59d415626 ("Drop remaining users of config.h") + * tests/screenshots: Use tab navigation to trigger the emergency calls menu. + The key combinations break with locale (and also when we use fading + labels which currently don't support use_underline) + * power-menu: Don't apply style from the emergency menu. + There are two emergency menu buttons. One in the power menu and one to + initiate the call in the emergency call menu. We only want the small + font size applied to the later one. + Fixes: dee478524 ("Add emergency menu system modal dialog") + * power-menu: Use fading label. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1042 + * ci: Drop 'gtk' from doc build. We're using gi-docgen since some time + * ci: Speed up doc build. No need for tests or tools here + * ci: Run doc-check + * ci: Use updated images + * wwan-info: Use correct base class as parent. + Fixes: ca1e70a95 ("Wwaninfo: Use StatusIcon base class") + * plugins/quicksettings: Set default icon-size. + This makes it match the other quick settings. + * monitor-manager: Add getter for night-light-supported property. + This makes is simpler for plugins to get the value independent + of future class hierarchy changes. + * shell: Make monitor manager available to plugins. + The night light plugin wants to know if night light support is + available. Other plugins might e.g. want to track the number of outputs + or similar in the future. + * plugins: Add night light toggle. + This quick settings enabled/disabled automatic night light. The setting + is disabled when no monitor is night light capable. + * build: Only override libsoup dependency when needed. + The next libsoup version will ship our fix as + https://gitlab.gnome.org/GNOME/libsoup/-/merge_requests/394 + got applied. + * top-panel: Don't init state twice + * build: Expand include path. + We add the subdirectories to the include path as we want e.g. + mkenums_simple to work out of the box (which uses the header's base + name) and we don't want clashing file names in subdirectories anyway. + * build: Simplify enum type generation. + Let's do away with the double accounting of headers. + * top-panel: Export state as property. + We have the enum and the getter already so let's make it + simple to track changes. + * shell: Track settings shell state too + `PhoshShellState` has a `Settings` flag but so far we never set it. + Augments: 59c8074c9 ("home: Update PhoshShellState") + * top-panel: Set initial shell state to unfolded. + This is an artifact of the fact that the drag surface code needs some + time to wiggle the surface into place (which we want to fix on the + compositor side). Once that is fixed we can then revert this patch. + * layout-manager: Return min padding in non-device mode. + If we don't want automatic layout based on device information we should + return the minimal padding rather than 0 as otherwise elements will be + too close to the screen edge. + While at that make it simpler to debug such situations. + Fixes: 474b07f90 ("layout-manager: Allow to query rounded corner information") + * layout-manager: Use min padding with zero radius. + A radius of zero means that there's no value present in the data. + This affect devices that have a panel information but no border radius + specified (like e.g. the Librem 5). + Fixes: 474b07f90 ("layout-manager: Allow to query rounded corner information") + * check-doc: Allow more files to raise DBus related gir warnings. + It's not a new warning but depending on compilation order different files + raise it. + * build: Split Wayland headers and sources + * build: Ensure plugins see the generated Wayland protocols. + * build: Separate generated sources and headers. + This avoids generating the C files multiple times reducing the number of + compilation units significantly. + This brings down the number of steps during the ci build from 3439 to + 1227 which build >60% less now. + * wall-clock: Make it a derivable class + * wall-clock: Allow to override wall clock. + Instead of having the mocked time within the production class allow to + override the relevant methods. The clock always returning a faked time + will uses this and will be added back in a follow up commit. + This will allow us to use another implementation in the tests + * wall-clock: Allow to format GDateTimes. + We simply wrap GnomeWallClocks functionality. + * tests: Add back a mocked clock that always returns constant time + * tests: Honor clock-format and clock-show-date GSettings. + These are relevant for screenshots. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1056 + * emergency-calls-manager: Don't leak name owner + g_dbus_proxy_get_name_owner() is transfer: full + * shell-manager: Don't leak the variant dict + * portal-access-manager: Don't leak icon + * ci: Update images. + We need to adjust some package names to the recent t64 transition in + Debian. + We fetch gcovr from Debian sid to work around the broken gcovr in + trixie. This can be reverted once gcovr migrated. + * ci: Use GNOME mirror for ci-fairy. + This reduces dependencies to external systems. + The mirror is slightly behind so we use an older commit but the ci-fairy + template is identical. + * monitor: Don't use anonymous structs. They confuse Rusts gir + * treewide: Add paths to dbus includes. + Most of the files did this but not all. This simplifies the pkgconfig + file as we don't need to add extra include paths. + * treewide: Split sources and headers. + We did this for the auto generated headers recently, let the rest + follow now. + * build: Fold in layer-surface sources. + We had those split out to make it more obvious what to grab for e.g. + squeekboard, phosh-osk-stub, etc but that's not needed anymore nowadays. + * build: Introduce private headers that aren't visible in gir. + This will allow us to split public and private headers more clearly in + the future. + We add files from contrib/ as don't have a matching prefix. We also add + it's users to avoid unknown type warnings in gir. This also drops the + classes from the docs. + * meson: Update libsoup dependency version. + There's now a libsoup release (3.5.1) containing or fix + + [ Arun Mani J ] + * util: Add phosh_util_matches_app_info. + Extracted from `app-grid.c` to be reused in folders. + * folder-info: Add `PhoshFolderInfo` + `PhoshFolderInfo` tracks the applications in a folder. + It implements some minimal `GAppInfo` virtual methods as per requirement. + * app-list-model: Group apps by folder. + We use `PhoshFolderInfo` to group apps by folders. To do this, we + prepend the folder item and remove all the apps belonging to the folder + from the `GList`. + We also monitor the folders available. + * favorite-list-model: Ignore `PhoshFolderInfo` in favorite check + `phosh_favorite_list_model_is_favorite` is used in the app grid to check + if the app must be displayed. + We return `FALSE` for `PhoshFolderInfo`. + * app-grid-base-button: Add `PhoshAppGridBaseButton` + The base class to share UI between `PhoshAppGridFolderButton` and + `PhoshAppGridButton`. + * app-grid-button: Port to PhoshAppGridBaseButton + * app-grid-folder-button: Add `PhoshAppGridFolderButton` + `PhoshAppGridFolderButton` is used to display the apps in + `PhoshFolderInfo`. We display them as images of size `DND` in a 2x2 grid. + * app-grid: Add support for folders. + We add a stack page which is shown when a folder needs to be shown. + Currently the folder is insensitive to searching. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/868 + * app-grid: Reindent `app-grid.ui` + Fix up indentation in a separate commit to help reviewing the previous + bits. + * app-grid: Use `STR_IS_NULL_OR_EMPTY` macro. + Replace empty or null string checks with the macro. + * app-grid: Use `phosh_util_matches_app_info` + We replace the search logic with the function from utilities to avoid + duplication. + * app-grid: Clean up `app-grid.ui` + By this commit we: + - Normalize property names to use hyphen instead of underscore. + - Remove setting property values to their defaults. + * folder-info: Add folders schema-id to the header + * app-list-model: Remove def of folders-schema-id + * folder-info: Reload apps when settings changes. + Previously, whenever GSettings' `app` key changes, we emit + `apps-changed` signal, so that app-grid and other dependents can regroup + apps if needed. + This had one issue. Since we don't reflect the changes inside + folder-info, if the folder is open, then the user doesn't see the new + apps. They had to close and reopen the folder, to see new apps. + In this commit, we reload the apps whenever the key changes in-addition + to signal emission. + * app-grid-folder-button: Set height to 64. + This is the height used by app-grid-button's image. Without this, if the + folders are the only buttons in grid, then they appear "shrunken". + * app-grid: Find the valid button to focus by loop. + Previously we focused either the last button or the second last button. + But this resulted in issues when multiple apps are removed. So to be on + safer side, find the valid button to focus by looping in + reverse. (Assuming that when main grid is shown again, it has at least + one button.) + * app-grid: Fix key-press stealing on open folder + * app-grid: Align the back button to center + * app-grid: Rename folder label to folder_name_label. + Keeps the name consistent with its purpose. + * app-grid-button: Add folder-info property. + This property can used to indicate that the app-grid-button is part of a + folder. Currently keeping it `write-only` to keep API simple. + * app-grid: Bind folder-info to buttons in folder + * folder-info: Add ability to set folder name. + This method acts as a proxy to the GSettings interface. + * app-grid: Add ability to edit folder names. + We add an entry that is shown when the edit button is toggled to active. When user + toggles the button again, the entry's contents are set as folder name. + Pressing Enter (or any activation) with the entry focused is same as + toggling button again. + Closing the folder (by pressing Escape or back-button or gesture) or + setting the entry to empty string doesn't do any changes, that is, + cancels the edit. + * util: Add methods to modify string array + * folder-info: Add ability to remove apps + * folder-info: Add ability to add apps + * app-grid-button: Add action to remove from folder. + This action is enabled only when the button has `folder-info` property + set. It is currently shown in the context-menu alongside other actions. + * app-grid-button: Add action to add to folder + * app-grid-button: Remove a folder when empty. + Remove a folder from `folder-children` if it is empty. + * settings: Request Wi-Fi scan on status page reveal. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1058 + + [ Sam Day ] + * build: Add option to build introspection libs + * build: Add NM-1.0 to phosh GIR includes + * build: Include plugin-shell.h in libphosh_sources. + This fixes many "Unresolved type: *PhoshShell" GIR errors. + Fixes: 505a5c8cb ("shell: Split out methods available to plugins") + * shell: Fix/add docstrings for all manager getters. + This fixes the various "Missing (transfer) annotation" complaints from + g-ir-scanner. + * ci: Add screenshot-diff requirements to image + * app-grid: Collapse favorites on init. + If there's no favorites, the favorites_changed callback is never fired, + which means there's an empty revealer (and separator) that will suddenly + collapse when a search is performed. + * wall-clock: Introduce PhoshWallClock. + This class wraps around two different GnomeWallClocks, one that provides + the time-only view of wall-clock time, and one that provides the full + (locale-specific) view of the time (including date). + Both the lockscreen and top-panel are updated to use this new class. + * wall-clock: Absorb phoc_util_local_date. + It is now phosh_wall_clock_local_date and requires a ref to a + PhoshWallClock. + Conveniently, the only places this method is being called is in signal + handlers that already have a ref to PWC. + * tests/screenshots: CSS override for animations + * ci: Output testlog artifact from screenshots job + * ci: Set G_MESSAGES_DEBUG=all for screenshots job + * feedback-manager: Warn about missing DBus svc once + * tests/screenshots: Robust waits on shell state. + Instead of relying solely on arbitrary sleep()s to ensure that the shell + is in the state we want before taking a screenshot, watch for changes to + PhoshShellStateFlags. + + [ Artur S0 ] + * Update Russian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Martin ] + * Update Slovenian translation + + [ Andika Triwidada ] + * Update Indonesian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Andi Chandler ] + * Update British English translation + + [ Yaron Shahrabani ] + * Update Hebrew translation + + [ Quentin PAGÈS ] + * Update Occitan translation + + [ Scrambled 777 ] + * Update Hindi translation + + -- Guido Günther Sun, 12 May 2024 13:41:38 +0200 + +phosh (0.38.0) experimental; urgency=medium + + + [ Guido Günther ] + * ci: Use consistent image version + * build: Update base image version in Dockerfile + * ci: Update images + * osd-window: Use G_PARAM_STATIC_STRINGS. + Fixes: 12983abe1 ("Add PhoshOsdWindow") + * osd-window: Hide level bar when level is < 0.0. + There's osd messages that don't need a bar (e.g. "Not available") + * shell-manager: Check if we got a level + * ui: Move width request to level-bar. + If we just have an icon we don't want to allocate the + space for the level bar. + * osd: Only show box if one of it's elements is present. + This makes sure the icon centers properly if only an icon is shown. + * osd-window: Use a larger icon when it's the only OSD element. + Otherwise it can be hard to spot. + * monitor: Only use wl_output_done. + Since xdg-output version 3 this is possible. + See wayland-protocols 962dd535372c8e4681374c23d2603cbe06cd7031 + * background: Use getters. + Easier to read and avoids g_object_get() round trip + * background-manager: Refresh the background on monitor configuration changes. + Otherwise we keep using the old pixmap which likely doesn't fit the + current scale or screen orientation. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1035 + * background: Exit early if layer surface isn't fully configured yet. + We can't query width or height if configuration isn't done. + * session: Only append session manager if supported by gnome-session + gnome-session dropped support for --builtin and --systemd. So don't + append that argument if it doesn't show up in gnome-session's help + output. + * build: Use systemd session manager by default + gnome-session dropped support for the builtin manager in 46. + * suspend-manger: No need to print error message twice + phosh_dbus_service_error_warn handles that for us. + * launcher-entry-manager: New object to handle launcher-entry protocol. + Listens for property updates. + * shell: Spawn launcher-entry-manager. + Spawn the manager from the shell and add accessors so other + parts can use it. + * launcher-box: Introduce launcher-item. + This will allow us to track an entries state + * launcher-box: Use launcher-item. + No new functionality for now + * launcher-item: Add properties to track progress and count + * launcher-box: Display progress and count. + We get the values from the launcher manager. + * top-panel: Properly name top bar height. + The height is the one of the bar not the panel (which is the bar plus + the settings) + * top-panel: Provide default icon size and min padding + * layout-manager: Allow to query rounded corner information. + Based on the panel height and the icon size provide the shift in pixels + from the screen edge to not occlude an icon. + * top-panel: Take rounded corners into account. + Shift icons more to the center if the rounded corners would occlude UI + elements. + We currently don't take into account that the area for the clock + placement gets slightly smaller due to this too. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1018 + * build: Add custom dependency for libsoup + libsoup sets `--export-dynamic` unconditionally (See + https://gitlab.gnome.org/GNOME/libsoup/-/merge_requests/394). + Since the dependencies are added past link_args we can't override it + with `--no-export-dynamic` so rather work around this by using a custom + dependency. + + [ Jesús Higueras ] + * build: Add dependency on libsoup. This will be used to decode base64 URIs. + * util: Add helper to decode data: image URIs + * media-player: Use phosh_util_data_uri_to_pixbuf helper for cover art decoding + + [ Daniel Rusek ] + * plugins: Add missing dot to the Comment field of launcher box lockscreen + plugin & switch to imperative form + * Update Czech translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Artur S0 ] + * Update Russian translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Martin ] + * Update Slovenian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Artur S0 ] + * Update Russian translation + + -- Guido Günther Wed, 03 Apr 2024 20:27:49 +0200 + +phosh (0.37.0) experimental; urgency=medium + + [ Arun Mani J ] + * wifi-network: Create PhoshWifiNetwork class. + This object is used to group access points based on their SSID, + encryption type and mode. + * util: Add function to get Wi-Fi icon name. + We add a new function `phosh_util_get_icon_by_wifi_strength` which can + be used to get the name of the icon corresponding to the strength and + connection status. + * wifi-manager: Track Wi-Fi networks. + We track the Wi-Fi networks in the Wi-Fi Manager. Only the access points + with a valid SSID is tracked. Also we define two new functions to + connect to an access point and request scan. + * wifi-network-row: Create PhoshWifiNetworkRow class. + A Wi-Fi network row represents a `PhoshWifiNetwork`. It tracks the + strength as well as SSID and displays them as an icon and label respectively. + * status-page: Create PhoshStatusPage class. + A status page is used in the quick settings to display more information + about a setting. For example, Wi-Fi quick setting can use it to show + available networks. + It must be subclassed accordingly and the header widget must be set as needed. + * wifi-status-page: Create PhoshWifiStatusPage class. + The status page for Wi-Fi displays Wi-Fi network rows. + * quick-setting: Allow to show status page + `has-status` is a boolean property that displays a `go-next` image + when true. This is used to indicate that long pressing the quick setting + opens up a status page. + * settings: Show Wi-Fi status page on long pressing Wi-Fi quick setting. + By this commit we add WifiStatusPage as a child to a stack. The flowbox + of PhoshQuickSettings is now a child of this stack. When a quick setting + is long pressed, the status page can be shown. + We also add a new action to close the status page. + * quick-setting: Add `present` property. + The `present` property is used to indicate if the quick setting is + available. Useful for hardware or mode dependent toggles. + * build: Export symbols for quick setting plugins + * build: Add option to build quick setting plugins + * plugins: Add support for quick setting plugins. + We add new extension points for quick setting and quick setting + preferences. + Then we adjust the build files to account for quick setting plugins. + * plugin: Add extension point for quick setting + * schema: Add quick-settings key to plugins schema + * settings: Add support for custom quick settings + * plugins: Add a simple custom quick setting + * tools: Add widget to test custom quick settings + * plugins: Drop G_LOG_DOMAIN in Calendar plugin + * tests: Load quick-setting plugins + + [ Guido Günther ] + * test-take-screenshots: Take the display out of power save mode. + When locking the screen the output is put into power saving mode. To + wake it up again the ScreenSaverManager sends "WakeupScreen" which is + caught by g-s-d. However we can't assume g-s-d to be available in the + tests so make sure the screen is powered as we can otherwise not take + any screenshots. + * wifi-manager: Use consistent filename. + PhoshWifiManager was one of phosh's first classes and hence lacks the '-' + where methods have a '_'. Fix this up for consistency reasons. + * wifi-manager: Rename cleanup_device. + We have a device tracking wifi networks and one tracking the active + connection. Make it obvious which one is being cleaned up from the + function name. + * wifi-manager: Rename check_device. + We have a device tracking wifi networks and one tracking the active + connection. Make it obvious which one is being cleaned up from the + function name. + * wifi-manager: Use Wi-Fi consistently. + Use the spelling introduced in + 9e39af9f7 ("wifi-manager: Track Wi-Fi networks") + consistently. + * treewide: Use consistent Wi-Fi spelling. + Use the official spelling. See previous commit for details. + * wifi-manager: Take a reference in dev. + We take a reference the Wi-Fi device as we otherwise race + with nmclient on shutdown. + This fixes the CI tests that otherwise occasionally hit + a critical on shutdown. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1025 + * wifi-manager: Fix indent. + This fixes some old heritage and makes the style check mostly happy. + * wifi-info: Use consistent file names. + We separate words with '-' in the filename since ages. + * wifi-status-page: Avoid underscore in property names. + We prefer dash ('-') + * wifi-status-page: Drop superfluous can-focus properties + * wifi-status-page: Disable selection. + We don't want to select the listbox row but merely activate it. This + avoids the blue highlighting of the activated row. + * status-page: Add a style class. + This allows styling status pages individually. Currently unused but + better to introduce it now as adding these later might break other + selectors. + * audio-settings: Disable listbox selection. + Otherwise we only want activation. This went unnoticed as we had + audio-device-row style class set which papered over it accidentally + style wise. + * audio-device-row: Drop css class name. + Otherwise the default style from listbox rows doesn't apply + * settings: Style list boxes more consistently. + Use a blue border for focus and make all icons symbolic. Eventually we + want to apply this to more of phosh's list boxes but as e.g. + notifications are list boxes too this requires more work upfrong. + * wifi-network-row: Drop symbolic suffix. + Allow settings-list-box icons are symbolic per CSS + * manager: Set name for idle callback + * shell: Name idle callbacks + * app-tracker: Name idle callback + * connectivity-info: Name idle callback + * monitor-manager: Name idle callback + * screen-saver-manager: Name idle callback + * status-icon: Name idle callback + * swipe-away-bin: Name idle callback + * lockscreen: Name timers + * main: Namer timer + * timestamp-label: Name timer + * screen-saver-manager: Name timer + * shell: Name timer + * screen-saver-manager: Add G_{BEGIN,END}_DECLS + * run_tools: Allow to override log domains + * build: Simplify libs. + We build a static lib for use with the phosh binaries, tests, etc and a + dynamic one for the tests but don't need an extra dependency. + Both libs use a common set of headers and if we link the main library we + don't need to link with tools too. + * plugins: Only use list-model headers. + Don't build the C files too. It's not needed as we can pull them from + phosh. + * build: Limit symbols exported from phosh + * build: Unify plugin header dependencies. + Make sure the plugins have a proper dependency on the headers and know + their paths. Since some of them are generated the build might fail + otherwise. + While at that make sure that all plugins use `plugin_dep` as common + dependency. + * tools: Add tool to verify exported symbols. + We want to make sure we don't leak all symbols to plugins + * tools: Make sure widget-box has listmodel + * plugins: Use dependency variables. + We initially used extra 'dependency()' to ease copy pasting for out of + tree plugin but since most plugins are in tree we can simplify this. + * testlib: Fix argument name + * main: Indicate readiness. + This makes it simpler to see from the logs if shell startup worked as + expected. + * shell: Simplify comment + * shell: Add transfer annotation for get_primary_monitor() + * shell: Add transfer annotation to get_builtin_monitor() + * background-manager: No need to sink ref. + It's a toplevel. + * screensaver-manager: Avoid warning when op was cancelled. + It's an async operation, not doing so might break the tests. + * main: Instantiate background cache + * tests/shell: Process events until idle. + Since the background manager will move it's setup in the idle callbacks + we need to process all pending events as otherwise it's hasn't been + created when the tests run. + * background-image: New object to hold a async loaded image. + This allows us to have an object that loads fully async by moving the + load operation into a thread. + * background-cache: Add cache for loaded background images. + The cache can load image async. When fetching an image the caller is + notified via the `image-present`. + * util: Add file equal check. + This is identical to g_file_equal() but handles `NULL` gracefully. + * background: Load images async. + Instead of handling loading in the background (and potentially blocking + drawing) load them async. For that we move the settings handling into + the background manager (which also handles clearing the cache on + settings changes). + As the background now calls back into the background-manager we finish + background-manager's setup in an idle callback to ensure the background + -manager is fully up when the background needs it. + This simplifies the background handling as the background-manager no + longer needs to be concerned with the details of the layer surface. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/447 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/910 + * background: Drop unneeded scale. + This was introduced in + 540c85c83 ("background: Scale by fractional output scale") + to handle the lack of fractional scale handling in + phosh_shell_get_usable_area() but that was fixed in + 1b24ca54c ("shell: Use fractional scale for phosh_shell_get_usable_area too") + This helps to decouple PhoshBackground from PhoshBackgroundManager. + * background-manager: Select dark wallpaper when available. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/743 + * build: Disable cast-align checks for clang. + These just add to the noise. This is similar to what we did for phoc + recently. + * treewide: Drop some unused variables. + Spotted by clang + * build: Don't use pointer for polkit check. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1023 + * tests/take-screenshots: Use milli second timer. + This allows us to use shorter waits + * tests/take-screenshot: Shorten wait intervals. + Most of the time we wait for a rather quick animation or keypress + to be processed. 0.5s should be ample of time. + * tools: Use canonical signal name for `delete-event` + * gmobile: Update to 0.0.6 + * lockscreen: Allow to keyboard navigate to lockscreen / calls page. + Since Phosh can't use GtkAppliction we can't use + gtk_application_set_accels_for_action() either (might be worth adding + this functionality at some point). Hence we process key events directly. + * tests/take-screenshots: Use one place to set gsettings + * tests/take-screenshots: Screenshot lockscreen plugins page. + We load some plugins for that. + * ci: Install plugins for screenshot tests + * treewide: Remove TEST_INSTALLED special handling. + CI installs into the default path now and we've never used it + for local testing. + * tests: Drop wl_output double accounting. + We have it in `monitor`. No need to have it separately. + * top-panel: Document on-lockscreen property constrains + * top-panel: Use Monitor instead of wl_output. + Use Phosh's types where possible (makes it consistent with e.g + PhoshBackground and PhoshSystemModal. + * top-panel: Set fb and bg colors in our own block. + Don't share it with phosh-home so we can tweak it independently + * top-panel: Use `self` correctly + `self` is the top-panel here, not the drag-surface. + * home: Fix 'state' property setter. + Make setting the property equivalent to using the property setter. + Modernize the property description while at that. + * home: Add property getter. + It's easier to read and faster. + * home: Use PhoshMonitor instead of wl_output. + Use Phosh's types where possible (makes it consistent with e.g + PhoshBackground and PhoshSystemModal. + * settings: Document on-lockscreen property constrains + * lockshield: Use more type-safe constructor variables + * lockscreen: Rename settings variable. + We use it for different lockscreen settings + * lockscreen: Rename notification specific settings + * background-manager: Emit signal when backgrounds need an update. + This allows us parts of the shell to react on these. + * emergency-menu: Use g_signal_connect_object. + This makes sure the signal handler gets disconnected when the menu gets + destroyed. Otherwise the list might call in a no longer existent menu. + * wwan-mm: Report 5G when combined with access technologies. + Check for 5G early as we want to report it when in use with e.g. 4G + + Tested-by: Luca Weiss + * plugins: Use new canonical resource path in examples. + See 8ec58e355 ("plugins: Add launcher plugin") + * build: Remove unused dbus doc targets + * build: Use preference specific include path. + It points to the same folder atm but we have both so use the + correct one. This allows us to have separate header include paths + for plugins and their preferences. + * status-icon: Document properties. + This hopefully makes it simpler for plugin authors. + * docs: Add URL map. + This allows us to link to docs in other projects + * quick-setting: Document automatic binding of `active` property + * quick-setting: Improve class documentation. + Use gi-docgen style links, fix signal name. + * suspend-manager: Use passed in values. + This allows us to use the same function for other use cases. + * suspend-manager: Use consistent Wi-Fi spelling. + Fixes: f228ca6e1 ("treewide: Use consistent Wi-Fi spelling") + * session-manager: Make inhibitor more flexible. + Allow to inhibit other things besides suspend. + * build: Split dbus header and source targets. + This allows us to only use the header dependencies for the plugins as + there's no point in generating the sources (which would then happen for + every plugin). + We only split the ones that already use our default namespace as we want + to fix up the namespaces for the other ones first as this gives shorter + names. See e.g. + aa6c9323b ("session-manager: Use default DBus prefix") + * quick-settings-standalone: Print which plugins are loaded. + Makes developing a plugin a bit simpler + * build: Introduce plugin dep. + This allows + * shell: Split out methods available to plugins + shell.h pulls in about almost all object's headers. In order to not leak + all of those to the plugins (requiring them to declare dependencies on + other libraries used by those headers like NM, gnome-desktop, etc) + introduce a separate header file that can be used by plugins to only get + a basic set. + * plugins: Add Types field to lockscreen plugins + * plugins: Add caffeine icon. + Icon taken form GNOME's icon library. + * plugins: Add caffeine quick setting. + This uses Arun's SimpleCustom quick setting as template. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/829 + * screen-saver-manager: Use g_timeout_add_seconds_once + * screen-saver-manager: Use lock-enabled in more places. + Except in cases where the lock is requested explicitly (like the DBus + call) we want to take the lock-enabled setting into account. + This fixes the case where lock-enabled is false but a lock-delay is set. + We don't want to lock in that case either. + * vpn-info: Ensure initial property sync. + Make sure we sync with the manager object. Otherwise we might end up + with stale state. + This fixes the vpn quick setting disappearing when moving the primary + output. + * build: Add quick-setting-plugins to summary + * docs: Mention .phoshdebug + * docs: Link to more manpages + * docs: Enable fatal warnings during doc build. + This makes sure we no longer ignore links that became stale or + are mistyped. + * build: Don't install files from gmobile. We link statically + * tools: Update symbol list + * data: Validate desktop file + * ci: Run tools tests too. + If we don't give a suite we'd miss the exported symbols check + * po: Add missing desktop files + + [ Sicelo A. Mhlongo ] + * wwan/ofono: Fix style + * wwan/ofono: Obtain operator name from network. + Prefer the operator name provided by the network, in place of the name + embedded in the SIM. This is especially important when roaming, so one + can know which network they are actually connected to. + + [ Rodney Lorrimar ] + * build: Declare gio-unix-2.0 dep where necessary. + The launcher-box and ticket-box plugin meson files didn't declare a + dependency on gio-unix-2.0. + + [ Danial Behzadi ] + * Update Persian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Florentina Mușat ] + * Update Romanian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Artur S0 ] + * Update Russian translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Pablo Barciela ] + * Update Spanish translation + + -- Guido Günther Wed, 06 Mar 2024 16:56:20 +0100 + +phosh (0.36.0) experimental; urgency=medium + + [ Sertonix ] + * util: Update broken app_id mappings + * util: Fix last_component lowercase + + [ Guido Günther ] + * ci: Use common ci template. + This removes an external reference and we want to share more common job + config. + * ci: Use common po check. + * home: No need to store osk manager. + We fetch it every time, so no need to store it. + * home: Fix signature of on_powerbar_pressed. + Osk manager and shell aren't passed since this is a GTK signal callback. + Fixes: fe308c2ce ("home: Shrink home-bar height and move osk activation to center") + * home: Allow to tweak osk long press delay + * lockscreen-manager: Don't try to clear already gone shields. + Uplugging a monitor can race with unlock (which discards all shields). + Make sure we don't try to destroy a shield for an unplugged monitor when + lockscreen_unlock_cb has just removed all shields as we crash + otherwise.. + * lockscreen: Drop hardcoded Cantarell font. + It was only on a single element. + * top-bar: Drop hardcoded Cantarell font. + Only one element on the lock screen as well as the top-bar had it + hardcoded making the font inconsistent when changed via + org.gnome.desktop.interface font-name + GSetting. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1016 + * monitor-manager: Sync gamma tables for new monitors. + This ensures newly plugged outputs get their gamma table updated and + hence e.g. pick up night light correctly. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1019 + * wayland: Drop unused input-inhibitor protocol. + It's unused and wlroots 0.18.x dropped support for it. + * tools: Parse additional license headers. + * home: Decruft a bit + * home: Drop animated arrow. + The overview can be closed by activating an application so there's no real + need for the arrow. + * home: Drop the home-bar's stack. + We don't need it anymore since the arrow is gone + * home: Hide power bar pill when unfolded. + Otherwise it might look a bit out of place as it's exactly over the + active app. + * gmobile: Update to 0.0.5 adding notch support for the FP5. + + [ Anna “CyberTailor” ] + * build: Allow elogind to be used on systems without SystemD + + [ Bardia Moshiri ] + * packaging: Add libgirepository1.0-dev as a build dependency + + [ Kai Lüke ] + * phosh-session: Drop ambient capabilities. + Systemd 254 adds cap_wake_alarm by default in pam_systemd. + The phosh.service makes use of it through PAMName=login. + This causes phoc to run with this ambient capability which is passed to + the Phosh session which then fails to spawn any .desktop files such as + Epiphany or any Flatpak app because they depend on bwrap which has a + restrictive check against additional capabilities: + https://github.com/containers/bubblewrap/issues/380 + A regular GNOME session with GDM and GNOME Shell does not have this + problem. + Drop ambient capabilities before running the Phosh session to resolve + the bwrap problem and align the behavior with the regular GNOME setup. + * phosh-session: Reduce number of processes. + The phoc -E switch uses "sh -c" to run the given commands and then in + that shell "bash -lc" is started to run the session. Without using + "exec CMD" this leaves the shell process around while they wait for the + child process. + To save memory, replace the shell process in the second invocation, but + leave the first as is to address this in phoc. + + [ Martin ] + * Update Slovenian translation + + [ Pierre Michel Augustin ] + * Update Haitian Creole translation + + -- Guido Günther Wed, 31 Jan 2024 11:16:42 +0100 + +phosh (0.35.0) experimental; urgency=medium + + [ Guido Günther ] + * osk-manager: Sync initial state. + Fixes 43d801d9da2dfe2b90f6b38068c3517d3e658d39 + Tested-by: Jarrah Gosbell + * util: Revert "Be more graceful on the app_id" + This breaks telegram desktop with app-id org.telegram.desktop and a + desktop file of org.telegram.desktop.desktop. + We *only* made that change to not change the behaviour of the + end-session-dialog. All inhibitors there do have a proper app-id though + so let's just live without not appending '.desktop' there either. + This reverts commit 2a3763303b4305058ee0ff5f2b76b31016e80114. + Reported-by: Newbyte + Tested-by: Newbyte + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1010 + * shell-manager: Drop support for XML version parsing. + Just assume we have GNOME_DESKTOP_PLATFORM_VERSION (introduced in GNOME + 43) + * build: Drop support for older gnome-desktop without the platform version. + This was introduced in 43. We keep the check for recent GNOME in the + summary() so it's obvious when things are outdated. + * build: Encode glib version in one place only. + We keep the check for recent glib in summary() so backports indicate + clearly when they don't have it. + This uses the same logic we use in phoc + * ci: Only run ci-fairy on merge-request pipelines targeting 'main' + Backports to older version can e.g. contain translations that don't need + to adhere to the commit formatting. + * build: Avoid quotes around boolean values + * build: Use project_build_root() as plain build_root() is deprecated + * build: Use project_source_root() as plain source_root() is deprecated + * build: Bump meson version to 1.0.0. + Debian stable has 1.0.1 even + * uncrustify: Fix while() brace alignment too. + We had it for `for` and `if` but `while` was missing + * build: Drop meson version check. + We require 1.0 now + * session-manager: Allow to inhibit suspend + * suspend-manager: Inhibit via gnome-session rather than systemd. + The systemd inhibitor would ensure we don't suspend when e.g. another + session is active. What we're interested about here is the user's + session though so use gnome-session to prevent suspend instead. + We use a sync DBus call here which should be fine and not block and we + can't do much without gnome-session running anyway. + For the systemd bits see https://github.com/systemd/systemd/issues/29818 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1001 + * idle-manager: Drop unused variable from struct + * idle-manager: Drop superfluous comment. + We explain this in the class documentation and have a comment in + the DBusWatch. + * screen-saver-manager: Clear long press timer in error path. + This shouldn't happen but if we hit it then we shouldn't create another + timer but rather clear the old one. + * emergency-info: Suggest word-completion in text views. + This allows the OSK to decide whether it should offer word completion + (or not). + * launcher-box: Fix leaks. + Don't leak enumerator, app-info and path + * upcoming-events: Don't leak color string from rgba conversion + * widget-box-standalone: Don't leak plugin dirs + * lockscreen: Add small margin to the widget box. + If there are long filenames in the ticket box it expands the widget box + right to the screen edge breaking the swipe back. + By adding a small margin we always allow a fling at the screen edge to + go back to the actual lockscreen. + + [ Bardia Moshiri ] + * wwan-ofono: Mark access technology nr as 5G + * wwan-mm: Mark access technology MM_MODEM_ACCESS_TECHNOLOGY_5GNR as 5G + + [ mathew-dennis ] + * home: Shrink home-bar height and move osk activation to center. + Reduce the home-bar size from 40px to 15px + Replace osk button with powerbar widget and move it to the center + Modify the on pressed function to work with long press gesture + Use osk-manager directly + Add an svg image to represent the new powerbar widget + Add both powerbar and arrow into a gtk stack since both are occupying the center region + Reuse phosh_home_update_osk_button to switch home-bar stack when dragging + Closes: https:// gitlab.gnome.org/World/Phosh/phosh/-/issues/380 + * home: Make powerbar more interactive. + Reduce powerbar opacity to indicate longpress recognition has started + And revert back to original state one recognition is successful + Add animation to indicate osk non-availability + Add haptic feedback to indicate a successful osk interaction + + [ Daniel Rusek ] + * Update Czech translation + + [ Artur S0 ] + * Update Russian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + -- Guido Günther Sat, 30 Dec 2023 19:28:08 +0100 + +phosh (0.34.0) experimental; urgency=medium + + [ Guido Günther ] + * ci: Switch to trixie and cleanup a bit + * d/control: Bump phoc dependency. + Needed for ext-idle-notify-v1 support. + * wayland: Add ext-idle-notify-v1. + This will replace org_kde_kwin_idle + * idle-manager: Use ext-idle-notify-v1 protocol + * wayland: Drop kde idle protocol. + Not supported by newer wlroots. + * idle-manager: Recreate timers when they need to be reset. + In contrast to KDE's idle protocol ext-idle-notify-v1 doesn't have a way to + reset individual idle notifiers so recreate them instead. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/994 + * HACKING: Add some more details. + Co-authored-by: Arun Mani J + * build: Bump glib to 2.76. We want g_autofd(). + * tools: Use G_APPLICATION_DEFAULT_FLAGS. Avoids deprecation warning. + * monitor-manager: Drop crtc gamma + * dbus: Add gsd color + * monitor-manager: Connect to gsd-color for night light information. + We set the gamma tables based on the color temperature read from gsd. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/875 + * testlib: Check for Wayland socket. + Check if the wayland socket is available. Gives a clearer error than + initializing GDK and hoping for the best. + * tests: Drop unused display argument of the shell fixture. + We run headless since some time + * Drop some unused variables + * tests/monitor-manager: Test more API + * osk-manager: Make availability depend on screen-keyboard-enabled. + The OSK is only really available when the gsetting is set (it might + still be toggle-able via explicit DBus calls but clients shouldn't rely + on that). + * lockscreen: Map keyboard button sensitiviy to OSK availability. + Only make the keyboard button on the keypad sensitive when the OSK would + actually unfold. + * home: Use osk-manager instead of tracking the gsetting directly. + This makes sure we don't show the button although there is no OSK + on the session DBus. It also ensures all parts of the shell get + their state from the osk-manager. + * subprojcts: Update gmobile to 0.0.4. + Use `git submodule sync` to update URLs in your working copy. + * subprojcts: Update libcall-ui to 0.1.1 + * call-notification: Use cui_call_format_duration + * call-notification: Use tnum font feature. + Keeps call duration constant width. + * monitor-manager: Use phosh_async_error_warn. + We don't want to print a warning when the DBus proxy creation failed. + * util: Be more graceful on the app_id. Don't care if it ends in .desktop + or not. + * end-session-dialog: Use cancellable. + As in other places properly cancel async calls during shutdown so we + don't need to hold a ref on self (which can then again trigger invalid + access as widgets are already disposes). + This also avoids a leak of self in the error path. + * end-session-dialog: Use phosh_get_desktop_app_info_for_app_id. + This makes for better information in the end session dialog + * end-session-dialog: Only show list box when we created inhibitors. + This allows us to skip inhibitors that make no sense + * end-session-dialog: Check flags before creating an entry. + Only create a row in the listbox when the inhibitor affects logout. + * end-session-dialog: Skip empty app-ids. + They're either from system services or stale entries + * end-session-dialog: Print app_id if we can't resolve the app info. + This gives at least some idea what is holding the inhibitor. We could + also do "Unknown application: %s" but that gets shortened too much + on mobile. + + [ Arun Mani J ] + * ci: Add Uncrustify to Debian image + * ci: Add Uncrustify config and checker + * ci: Run style checks on merge requests. + * ci: Move `sanity` job to `style-checks` stage. + + [ Artur S0 ] + * Update Russian translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Juliano de Souza Camargo ] + * Update Brazilian Portuguese translation + + -- Guido Günther Mon, 27 Nov 2023 11:18:11 +0100 + +phosh (0.33.0) experimental; urgency=medium + + [ Guido Günther ] + * docs: Drop phosh-docs.xml. + It's unused since some time + * tools: Drop layer-shell-ui-edit. + Unused since ages + * plugins: Add launcher plugin. + This allows to use arbitrary desktop files as launchers on the lock + screen. See 8ec58e3555226c3616deb854eee4dd5833a67496 for details. + + [ pseudorandom-x ] + * password-entry: Add new widget. + A password entry that allows users + to toggle the visibility of typed password. + This is based on code from PhoshNetworkAuthPrompt. + * gtk-mount-prompt: Use password-entry. + Replaces GtkEntry with PhoshPasswordEntry, + as well as removes the callback and its + connection to toggle password visibility. + * network-auth-prompt: Use password-entry. + Replaces GtkEntry with PhoshPasswordEntry, + for both WiFi dialog box & VPN network + prompt. The callback toggling the visibility + of typed password is also removed. + * polkit-auth-prompt: Use password-entry. + Replaces GtkEntry with PhoshPasswordEntry. + * system-prompt: Use password-entry. + Replaces GtkEntry with PhoshPasswordEntry, + in both password entry and confirmation fields. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/848 + + [ Florentina Mușat ] + * Update Romanian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + -- Guido Günther Mon, 30 Oct 2023 19:08:23 +0100 + +phosh (0.32.0) experimental; urgency=medium + + [ Guido Günther ] + * session: Warn on deprecated rootston.ini. This way we can remove it in + the future + * phosh-session: Allow to include a .phoshdebug. + This allows to set e.g. G_MESSAGES_DEBUG or the path to a phoc.ini + independent from how phoc is started (command line, display manager, + systemd unit). + * docs: Mention phoc configuration file + * notification: Link to the official spec + * notifications: Merge comments + * notification: Add profile property. + This will be used to store the feedback profile level for the feedback + event associated with this notification. + * notify-feedback: Adjust feedback profile based on notification. + This allows the notification to lower (or skip) the event + feedback noisiness. + * notify-manager: Honor x-phosh-fb-profile hint. + Allowed values are feedback profile levels from the Feedback Profile + Spec and `none` to indicate: omit all feedback. + This is somewhat similar to the `suppress-sound` hint of the fdo spec. + * tools: Add category and profile to notification tool + * docs: Add some hints for application developers. Document some mobile + specific extensions. + * docs: Document additional notification hint + * docs: Mention that we prefer a "recipe" style git commit history + * docs: Add some more common patterns + * docs: Add a merge request check list + * feedback-manager: Don't access lfb if not inited. + Otherwise libfeedback asserts. + * feedback-manager: Add more type checks + * notification-feedback: Guard against unitted libfeedback + + [ Keegan Sabo ] + * app-grid: Hide separator when there are no favorites. + Hide the favorites separator if there are no favorites. + + [ Tjipke van der Heide ] + * Add Frisian translation + + [ Vasil Pupkin ] + * Add Belarusian translation + + -- Guido Günther Mon, 02 Oct 2023 14:17:40 +0200 + +phosh (0.31.1) experimental; urgency=medium + + * build: Fix version in meson file + + -- Guido Günther Mon, 04 Sep 2023 16:49:20 +0200 + +phosh (0.31.0) experimental; urgency=medium + + [ Guido Günther ] + * data: Add portals default config. + We keep the `UseIn` in phosh.portal for the moment to support + older xdg-desktop-portal. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/979 + Thanks to Pablo Correa Gomez for bringing this up + * portals: End lines with a semicolon. + It's a desktop file like syntax + * portals: Use wlr as fallback for screenshot portal. + E.g. Debian has it disabled for the gtk portal. + * portal: Disable screencast portal. No implementation yet. + * portal: Add secrets portal. + Provided by gnome-keyring + * wayland: Make zwlr_output_power_manager_v1 mandatory. + It's there since phoc 0.1.6. + * suspend-manager: Drop unused variable + * protocol: Add phoc-device-state-unstable-v1. + Track the device state capabilities and merge them into the seat's caps. + This way we only need to worry about one set of capabilities. + * mode-manager: Set tablet or laptop mode for convertibles. + Based on a patch by Jonathan Hall. + Closes: https://gitlab.gnome.org/World/Phosh/phoc/-/issues/320 + * mode-manager: Switch between tablet and laptop mode. + When the seat as the needed caps we register a listener for the + switch and switch between tablet and laptop mode based on its state. + * ci: Update variables. We moved to trixie + + [ Simon McVittie ] + * data: Install phosh-portals-conf in /usr/share. + This is supported in xdg-desktop-portal >= 0.17.1 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/982 + + [ Boyuan Yang ] + * Add Chinese (China) translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Fran Dieguez ] + * Update Galician translation + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Asier Sarasua Garmendia ] + * Update Basque translation + + [ Artur S0 ] + * Update Russian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Martin ] + * Update Slovenian translation + + [ Jürgen Benvenuti ] + * Update German translation + + -- Guido Günther Mon, 04 Sep 2023 13:33:50 +0200 + +phosh (0.30.0) experimental; urgency=medium + + [ Guido Günther ] + * toplevel-manager: Drop set_property. It's unused + * toplevel-manager: Fix indent + * toplevel: Fix some indent + * toplevel: Move _init upwards. + We have it past _class_init. See HACKING.md. + * toplevel: Drop initialization. + They're inited to 0 per default. + * app-tracker: Drop unused variable + * data: Add icon for audio-handsfree. + This happens when e.g. connecting to a car's audio unit. For now we just + use the plain headset icon. See PA commit + https://gitlab.freedesktop.org/pulseaudio/pulseaudio/-/commit/28069ef0f74089ae18cefbe7e97d2b920d7ba0cc + Once we have one in the adwaita-icon theme we can drop the icon here. + * call-notification: Make sure we always show a phone number when available. + For calls that don't have an entry in the address book we want to make + sure we *only* show the phone number and nothing else (not e.g. "Unknown + Caller") while the reset of the logic (whether to show initials, etc) + should stay the same. + * Update gvc submodule. + This fixes a crash when pa or pipewire-pulse restarts. + * README: Mention git submodules + * README: Use meson instead of ninja + * ci: Give asan suite some more time. + It passes but needs a bit longer. + * ci: Speedup unit tests by skipping screenshots. + We run the screenshot tests anyway so instead of running them twice + rather merge the test data. This uses less CPU and avoids the testsuite + sometimes failing because unit tests and screenshot tests step on each + others toes. + * lockscreen: Simplify clock css. + No need to have the class twice + * lockscreen: Move large clock down a bit. + WIth the rework we lost a bit of vertical padding. Add this back but + only when we use the large clock. + Fixes 515ccf982b9acbd6bae5f97d79fb1115190303f4 + * data: Drop gsd-xsettings from required components. + The session is very functional without. + As newer wlroots looks for a surface and there is non it gets + disconnected when we don't force XWayland to stick around which we don't + want to. + * lockscreen: Don't scroll back to clock page while auth is running. + Going back to the clock page while the auth thread is running is + confusing for the user. This can happen when pam wants to slow down + entering a new password for more than 5 seconds. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/968 + Tested-by: Teemu Ikonen + * gmobile: Update to 0.0.3 This brings support for the FF4's and Poco F1 + notches + + [ anteater ] + * screenshot-manager: Save screenshots at full resolution. + This has to normalize scale to join monitors, but can at least + normalize to the largest scale present rather than 1.0. + Screenshots of any single monitor will be saved at native scale, + and screenshots spanning multiple monitors will scale them each up + to the maximum scale of a monitor present in the screenshot. + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/946 + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Martin ] + * Update Slovenian translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + -- Guido Günther Tue, 01 Aug 2023 12:13:54 +0200 + +phosh (0.29.0) experimental; urgency=medium + + [ Guido Günther ] + * tests: Create a dummy object for PhoshShell. + * phosh-wayland: Drop feature flags. + Wayland defines these just fine for us from the protocol definitions. + This makes it consistent with other places where we used the + wayland definitions directly e.g. + PHOSH_PRIVATE_KEYBOARD_EVENT_ACCELERATOR_RELEASED_EVENT_SINCE_VERSION + ZPHOC_LAYER_SHELL_EFFECTS_V1_GET_ALPHA_LAYER_SURFACE_SINCE_VERSION + * notify-feedback: Exit early when we woke up the screen. Doing it once is + enough. + * notify-feedback: Use PhoshNotificationSource. + No functional change, just make the function take a notification source + and not assume it's part of an on_items_changed callback. + * notify-feedback: Move event based feedback emission into a separate function. + Iterating a notification source to look for feedback to emit will be + used in more places. + * notify-feedback: Don't skip feedback if we fail to get the app-id. + Use phosh's current feedback level in that case. + * notify-feedback: Blink leds when screen goes blank and there are notifications. + If there are notifications present and the screen goes blank we should + explicitly trigger event feedback for these. + * ambient: Avoid initial theme switch + * animation: Add enum type for the animation types. + This allows us to use them as properties. + * layer-surface: Move alpha handling here. + To avoid having to pass around even more properties we use + phosh_shell_get_default () which makes this a bit harder to use in other + programs like phosh-osk-stub or squeekboard. + This will allow us to use alpha blending in other parts without much + code. + * fader: Make fade-out configurable. + Add properties to configure fade-out length and type + Helps: #940 + * ambient: Use a fader to smooth the theme change. + * timestamp-label: Don't leak datetime + * phosh-background: Simplify. Just chain up without a parent_class variable + * tests: Don't leak monitor. + The tests that run against phoc leaked the created monitor object. + * tests/stubs: Only create toplevel-manager once. + This helps test-overview + * leak-suppress: Drop old leak suppression. + I fixed that in GTK via b1b9de6836a19cb6336b6a20ad652afc14f1e2b2 in + 2019. + * leak-suppress: Add new GTK, GDK and gvfs leaks. + * tests/end-session-dialog: Make both tests more alike. + Drop superfluous g_object_ref_sink and use gtk_widget_destroy + to fix resource leaks. + * tests/notification: Fix leak + * tests/favourite-model: Fix leak + g_list_model_get_item returns a ref + * tests/app-grid-button: Fix leaks + * tests/stubs: Make shell variable static. Tests use phosh_shell_get_default() + * ci: Use specific asan image. + This allows us to have debugging symbols in there so the leak + suppressions match but without making the base image larger. + * settings: Simplify brightness slider ui bits. + * settings: Simplify torch slider ui bits + * settings: Drop packing from settings box. + * lockscreen: Use G_SOURCE_REMOVE + * lockscreen: Queue redraw for clock when deck moves back to carousel. + Otherwise the clock might show an outdated time after phone calls or + when swiping back from the plugins page. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/922 + * system-modal-dialog: Allow to fade out. + This is similar to what we do with splash screens but we use + an ease out animation to give a rapid response to the user + interaction. + * system-prompt: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * phosh-end-session-dialog: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * polkit-auth-agent: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * network-auth-manager: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * run-command-manager: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * emergency-calls-manager: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * gtk-mount-manager: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * app-auth-prompt: Use phosh_system_modal_dialog_close() + This will use a fadeout animation if available + * tests: Drop unused variables + * ci: Switch to package test to trixie. + Bookworm is released and we want to build against latest libraries. + * tests/gtk-mount-manager: Disable animations. + Otherwise it might still be running when we end the main loop. + * Update gvc submodule first time since 2018. + Avoid passing NULL to gvc_mixer_control_get_vol_* as this raises a + critical in newer versions. Use PAs definitions directly instead. + * channel-bar: Simplify and modernize + * channel-bar: Use symbolic icons via CSS. + This will allow us to use non symbolic icon names from gvc directly. + * channel-bar: Load icons with default fallbacks. + This makes sure we get a valid icon for things like + `audio-headset-analog-usb` + * channel-bar: Use a ui file + * settings: Fix enum type. We're setting the RotationManagerMode here. + * settings: Separate concerns. Move headphone detection and stream pausing + into separate functions + * settings: Get audio output icon from stream. + This makes us use different icons for e.g. HDMI and USB headsets making + it easier to figure out which output is being changed. + * emergency-info-prefs: Fix indent. + Use 2 space indent consistently, avoid space after `->` + * emergency-info-prefs: Avoid gint. See CodingStyle + * emergency-info-prefs: Use (const char * const *) consistently + * emergency-info-prefs: Avoid saving empty values for string arrays. + If allergies or medical conditions are empty there's no need to + save the values. + * emergency-info-prefs: Build keyfile path only once. + We use a single keyfile so build it's path only once at object creation + time. + * emergency-info-prefs: Don't return early if there's no keyfile. + We can just create it instead of exiting early which made things appear + as if "add" wouldn't do anything. + Thanks to Jonathan for the report (Closes: #955) + * emergency-info-prefs: Move keyfile saving to a single function + * emergency-info-prefs: Create directories when missing + * emergency-info-prefs: Set empty string when value is missing. + When the value is missing in the keyfile set the empty string to + avoid warnings when loading the keyfile. + * emergency-info-prefs: Only make "Add" sensitive with contact. + We only make the "Add" sensitive when there is a contact + * emergency-info-prefs: Allow for contacts without a number. + The user can add them later so instead of silently ignoring this contact + add it without a number. + * emergency-info-prefs: Be more verbose when saving fails + * Update gbmobile submodule. + Adds cutuout information for the xiami,beryllium + * shell: Remember the settings object. We'll use it in more places in the + future + * shell: Move define past includes according to coding style + * shell: Don't make debug flags dependent on shell objects. + This allows us to use it from other parts without + phosh_shell_get_default(). + * monitor: Allow to fake builtin display. + Allow the virtual displays to be treated as built-in displays via to + `PHOSH_DEBUG=fake-builtin`. This can be useful e.g. to debug top-panel + layouts. + * monitor: Make preferred mode check simple. Make it simple to check if the + current mode of a monitor is the preferred mode. + * tests/data: Add an overlay matching the nested wayland mode. + * top-panel: Use height internally. No need to have it passed in at + construction time as we don't handle different sizes anyway. + * Add layout manager. The layout manager is responsible for providing + layout information to e.g. avoid notches and rounded corners. + We currently imply a notch in the upper screen area that mostly affects + normal orientation hence ignoring it in landscape. + * top-panel: Use layout manager to place clocks. + We place the center clock to the left or right when needed and shift + down the clock in the settings menu. + * tests: Screenshot with notch + * calls-manager: Avoid deprecated calls state. + Those were dropped from calls a while ago. This made it impossible for + the user to get back to an incoming (but not yet accepted) call on the + lock screen. + * packaging: Bump calls dependency. + We want to use a calls version that matches our enum values. + * media-player: Connect player right away. + No need to wait for the idle callback. While at that use + current naming. + * media-player: Modernize property definitions + * lockscreen: Drop gtk_widget_realize() in constructed. This just confuses + sizes calculations + * lockscreen: Modernize UI file a bit + * lockscreen: Split call settings and page changes. Follow up commits want + to set the call but not switch to the call ui page. + * lockscreen: Remove superfluous titlebar element + * lockscreen: Move style class toggling to separate function + * lockscreen: Always toggle notification revealer. + Instead of toggling visibility on the list and the scrolled window just + use the revealer. This is less code and makes sure things are consistent + between no notifications and disabled lockscreen notifications. + * lockscreen: Use a revealer for the media-player. + This makes it consistent with the notifications + * lockscreen: Use small clock when media-player widget is visible too. + So far we only used the small clock when notifications where visible + but from the UI point of view there's no difference between those + so use the same layout. + This also makes it simpler to add more info elements. + * lockscreen: Move media player and notifications into one scrolled window. + This avoids half cut notifications at the top, etc. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/593 + * lockscreen: Drop max content height. + Let's have the scrolled window use as much space as possible to fit the + notifations. + * media-player: Modernize UI file a bit + * call: Don't lookup icon for empty string. + * notifications: Reduce bottom padding of notification tray. 48px wastes + lots of space. + * lockscreen: Move deck variable. It's not part of the call page + * lockscreen: Use set_visible_child. This way we don't have to check if + we're on the right page and can't overshoot. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/890 + * lockscreen: Drop p-large style class for clock. + We toggle p-small on and off so we don't need another one for + the default. + * calls-manager: Expose calls as list model. This makes it simple to have + them in list boxes. For now we just proxy everything from an embedded + GListStore. + * lockscreen: Add call notifications. + This makes it simple to go back to an ongoing call and will allow + us to handle multiple calls easily. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/591 + * gvc-channel-bar: Guard against NULL icon. + Fallback to speaker if we don't have more information. + * settings: Don't crash on NULL port. + Can happen with new gvc when switching VTs + Fixes: bf9d98a519fc0c4a9e3d0dffc6a4066508577b39 + * settings: Don't forget to mute stream. + Fixes e826ea4abca36042fbd77abe4e97dcab57b7a789 + * settings: Set channel-bar via UI file + * settings: Init parent class early in constructed + * settings: Use '-' for property names. Just for consistency + * settings: Bind settings-done via ui file. + Be consistent with notify::drag-handle-offset + * channel-bar: Use G_PARAM_STATIC_STRINGS + * channel-bar: Switch all props to G_PARAM_EXPLICIT_NOTIFY + * top-panel: Move click gesture to target phase. + This makes sure the panel is only closed when we click + outside the widget area, not when e.g. switching sound + devices. + * settings: Allow to select audio devices. This can be useful to switch + between headsets and speaker without having to open g-c-c. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/951 + * audio-settings: Close details when we launch the g-c-c panel + + [ undef ] + * top-panel: Allow device suspend from power menu. + Add suspend from the top-panel power menu alongside shutdown and + logout. This allows far quicker access than waiting for the automatic + timeout or typing the command. + + [ Sam Hewitt ] + * settings: Style audio device selection + * data: Add icon for suspend + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Prasanta Hembram ] + * Update Santali translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Martin ] + * Update Slovenian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + -- Guido Günther Wed, 07 Jun 2023 20:25:17 +0200 + +phosh (0.28.0) experimental; urgency=medium + + [ Eugenio Paolantonio (g7) ] + * packaging: Remove hardcoded build directory when running tests. + Rely on debhelper to determine the build directory, which may not be + `_build` anymore (see also 6483f73ea9018b95cf82f69242ce9bec4690e3de). + + [ Guido Günther ] + * libcall-ui: Switch to 0.1.0 + * app-tracker: Update flag descriptions. + We handle xdg-activation on the compositor side since some time + * emergency-calls-manager: Name functions consistently. + It's the emergency-calls manager, not the emergency-contact manager + * screen-saver-manager: Use G_SOURCE_REMOVE. + It's easier to parse than `FALSE`. + * end-session-dialog: Hide warning label when there are no inhibitors. + Fixes: 381efd906f411f4f738daeea43bde4f35bf3e2a0 + * notify-feedback: Look at correct notification. + So far we only ever looked at the first one. This is fine as + we break the loop on the first iteration atm but will lead + to surprises when we change this. + * notify-feedback: Avoid critical when app-info lookup fails + * ci: Montage screenshots via script. + This makes it easy to reproduce things locally + * system-modal-dialog: Improve title padding. + Add some margin to the left and right to make the title not touch the + border of the dialog. + * system-modal-dialog: Add top and bottom margins. + Otherwise the dialog can stretch to the screen edge + * system-modal-dialogs: Reorder the right box. + We want to reorder the button box when forcing a position. This + is so far unused. + * tools/check-access-portal: Simplify. + Let getopt handle '-h' and use a here document for easier formatting + * portal-access-manager: Don't leak grant and deny label. + They were g_strdup'ed but never freed. Instead use a pointer + to the gvariant's value. + * system-modal-dialog: Add property docstring. + Makes it show up in the docs + * app-auth-prompt: Add bindings for construct-only properties earlier. + Doing this in `constructed` worked by accident. When + `system-modal-dialog` removed it's binding in `init` it started to fail. + Fixes: 5f7d38bdf86c5f01e8cc0f1560181a875f68b34a (which didn't have a + chance to notice) + * system-modal-dialog: Simplify. + If we hide the title initially we only need to make it visible when we + set a title and can hence exit early if the title didn't change. + * system-modal-dialog: Drop constructed. + We can do that in init just fine + * ambient: Don't ignore unknown units silently. + Print at warning so support can be added when we hit that case. + * ci: Use uppercase variable name for alpine deps. + Fixes: 47209e49923a256432ca7e536ed7fa3eb20c7683 + * stubs: Use correct enum type for state flags + Labels, clamps and boxes are unfocusable by default + * lockscreen: Remove superfluous packaging. + Just use the defaults + * layersurface: Add noop implementation for configure. + This frees deriving classes from checking for NULL + * lockscreen: Drop the top margin on smaller displays. + This allows us to keep the keypad's box center aligned. + * test-notification: Use g_strv_equal. + We require glib 2.66 + * data: Sort by schema path + * screen-saver-manager: Add action to wakeup the screen + * shell: Make activating actions a bit more failsafe. + Make sure we log an error on inexistent actions. We remove the + action_group iface implementation for that. + * notify-feedback: Unblank screen on new notifications. + We allow to filter by urgency and category. This allows to e.g. enable + screen wakeup for all 'critical' messages and those send by alarm-clocks + but not instant-messages via + wakeup-screen-triggers == [ 'urgency', 'category' ] + wakeup-screen-urgency == 'critical' + wakeup-screen-categories == [ 'x-gnome.clock.alarm' ] + (assuming the alarm clock sets the above category) + To match on all instant messages too we'd do: + wakeup-screen-categories == [ 'x-gnome.clock.alarm', 'im' ] + This will match on all notifications with category class `im` + independent from the specific. So we'd match on `im`, `im.received` and + `im.error`. + Available categories (of the form `class.specific`) can be found in the + xdg spec: + https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/934 + * Revert "top-panel: Close settings before taking screenshot" + This reverts commit 30167a934b5b6820b583d262a386bed3d1dab141. + Not needed as we switch away from the power-menu. + * Revert "top-panel: Remove currently unused actions" + This reverts commit a4d2e6d846a2ffd4f3028242ed1803563ee68d17. + Add them back as we switch away from the power-menu. + * Revert "top-panel: Trigger power-menu" + This reverts commit 63fae83fa5ef42ade7fb898164fc7cb2d27c40a7. + Bring back the old power off / logout / restart menu + This closes https://gitlab.gnome.org/World/Phosh/phosh/-/issues/932 + although we don't add it to the power-button menu but rather bring + back the system menu. + * top-panel: Rename to menu to system-menu. + This better fits designer language (see + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1254) + * system-menu: Drop lock screen. + We have that on an extra button and it's not "power cycle" related. + * top-panel: Drop unused power-menu action + * system-menu: Use icons in the menu + * layersurface: Allow to set alpha + * splash-manager: Assume color-scheme is a known key + * build: Require settings-schema 42. + This allows us to drop the check for color-scheme + * splash: Remove excessive white space + * splash-manager: Hide splash. + Don't just destroy the widget. Give the splash a chance to fade out. + * animation: Add quintic ease in and out + * splash: Add phosh_splash_hide. + This hides and destroys the splash + * splash: Use a fade out animation to hide the splash + * splash: Allow to run against old phoc + * build: Add animation slowdown option. + For debugging it can be useful to slow down animations. Add config + option for that + * quicksettings: Add a bit more top and bottom padding. + This makes the buttons a bit easier to touch + * home: Ease application switching with super key. + Currently the super key activates the application view (focusing the the + search bar). Focusing the running application instead allows for direct + window switching via cursor keys and application search as the user can + just start typing. + * build: Bump supported glib version to 2.72. + We mostly do this to so we can assume `launch-started` in some + upcoming app-tracker changes. + This is still pretty conservative as glib 2.76 is current. + + [ Thomas Booker ] + * emergency-calls-manager: Fix spelling + + [ Alistair Francis ] + * home: Handle the super key. + Since Phoc allows passing the super key (see MR) we can subscribe to it + and use the super key to unfold the home. This is similar to how the key + works on GNOME. + Note that on versions of Phoc that don't expose the super key, this will + just generate a warning as Phoc rejects the binding. + Works-with: https://gitlab.gnome.org/World/Phosh/phoc/-/merge_requests/447 + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/563 + + [ Suraj Kumar Mahto ] + * lockscreen: Remove currently unused (and hidden) emergency call button. + Emergency calls are reachable via long press. We'll look into adding a + button right on the lock screen once the feature is deemded stable and + hence enabled by default. + + [ Sam Hewitt ] + * New close button for power menu + * top-panel: System menu styling + + [ Evangelos Ribeiro Tzaras ] + * gnome-shell-manager: Handle repeat for accelerators. + Repeat action until accelerator was released unless accelerator + was grabbed with "ignore autorepeat" set. + Fixes: #525 + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Pablo Correa Gómez ] + * Update Spanish translation + + [ Jordi Mas i Hernandez ] + * Update Catalan translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Daniel Rusek ] + * Update Czech translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Michael Oppliger ] + * Update German translation + + -- Guido Günther Mon, 29 May 2023 12:31:28 +0200 + +phosh (0.27.0) experimental; urgency=medium + + [ CoderThomasB ] + * Add emergency calls manager. + The emergency calls manager is respsonsible for fetching emergency + contacts from gnome-calls as well as initiating emergency calls. + * Add emergency menu system modal dialog. + A new modal dialog triggered by the power-button long press menu that is + used to dial emergency call numbers and to select emergency contacts. + * tests/screenshot: Test emergency menu. + Closes: https://gitlab.gnome.org/GNOME/calls/-/issues/30 + + [ Guido Günther ] + * tests: Make phosh_test_remove_tree() public. + Useful in non-shell tests too + * tests: Run timestamp label test without compositor. + Not needed, plain widget. + * tests: Add helpers to run tests against a compositor. + This was so far open coded in the different tests. The helpers + ensure we also get a separate DBus session. + * testlib/compositor: Shut down compositor before DBus. + This makes sure we can process all events + * testlib: Drop double assignment. + We do the same two lines above. + * testlib: Log when we connected to the running compositor + * tests: Use compositor helper for phoc based tests. + Simplifies the code but also fixes tests against newer phoc (0.26.0) + which needs to be able to spawn a DBus session. + * tests: Use compositor helper for integration tests + * tests: Add and use PHOSH_FULL_SHELL_TEST_ADD. + Just to simplify the code. Similar in spirit to + PHOSH_COMPOSITOR_TEST_ADD + * tests: Use full-shell fixture for idle-manager. + Less code and proper DBus isolation + * tests: Use full-shell fixture for gtk-mount-manager. + Less code and proper DBus isolation + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/671 + * ci: Slightly bump test timeout. + The screenshot tests are sometimes taking a bit longer. Bump the test + timeout a bit until we made them faster by reducing timeouts. + * ci: Drop the i386 crossbuild. + We could disable doc, tests, plugins and calendar-server to get away + without any gobject-introspection libs but that wouldn't leave much. + * ci: Update image before installing dependencies + * ci: Install phoc from Debian experimental. + We need it for the keyboard shortcut long press support + * ci: Use updated images + * tests: Run all tests in parallel. + Now with proper test isolation we can run all the tests in parallel + speeding up the pipeline. + * suspend-manager: Actually cancel the async call on shutdown. + The cancellable was NULL hence nothing happened. + * screen-saver-manager: Check async result first. + Check the result of the async call before accessing the screen-saver + manager. It might have already been disposed (due to shutdown). + Fixes another spurious test failure. + * screen-saver-manager: Init power button fd. + Make sure it's set < 0 as otherwise we might close it accidentally + on shutdown when getting the inhibitor got cancelled early (e.g. + in tests). + * proximity: Don't access proximity before checking async call result. + This can happen in tests when we cancel before actually connecting. + * screen-saver-manager: Emit signal on power button long press + * shell: Allow to fetch screenshot-manager. + We could avoid this if we pass it in when constructing the + power-menu-manager. + * screenshot-manager: Don't notify completion on DBus for internal calls + * screenshot-manager: Make do_screenshot() public. + This allows other parts of the shell to take a screen shot. + * shell: Implement ActionGroup and ActionMap interfaces. + This allows us to add global actions easily thus avoiding the need + to add proxy functions. + Inspired by what GtkApplication does. + * system-modal-dialog: Move the padding from title to the dialog. + This ensures that dialogues without title get the same padding. This + will be useful for the power-menu. + * system-modal-dialog: Hide title if empty. + Some dialogs don't have a title and this avoids a large empty + bar at the top. + * resources: Sort icons alphabetically + * Add power-dialog and manager. + To be shown on power button press or by activating the power button in + settings. This is currently used for screenshots, power-off, screen lock + and the upcoming emergency call support. + * top-panel: Trigger power-menu. + Instead of the somewhat fiddly power-off/reboot/... menu use + the more phone friendly power-button menu. + * shell: Toggle power-menu on power button long press + * settings: Remove unused template binding + * settings: Crossfade empty notification state. + Addresses parts point 2 of + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/788 + * mount-notification: Drop unused variable + * notify-manager: Make it simpler to add shell notifications. + Make it a single call to add a notification without actions. + * screenshot-manager: Add notification when taking a screenshot. + When the screenshot isin't triggered via DBus show a notification + that the result was copied to the clipboard. + * testlib: Allow to press key with timeout + * revealer: Fix gtk doc string + * system-modal-dialog: Fix signal name + * power-menu: Wire up emergency calls + * emergency-calls: Disable by default. + We want to make sure the code is working reliably. For that we + need a stable gnome-calls release and testing among more providers. + Hence toggle it off by default atm until that happened. It can + be enabled anytime using: + gsettings set sm.puri.phosh.emergency-calls enabled true + Distributions that want to enable it by default can use an override. + We do this in separate commit so that we can easily revert it + once we want to enable by default. + * tests: Add more items to end-session dialog. + This allows to check overflow + * end-session-dialog: Wrap listbox in scrolled window. + Avoids overflow with many items + * rotation-manager: Use consistent naming. + We call it accelerometer in other places + * rotation-manager: Look at blanked state for claim/unclaim. + No need to keep the accelerometer claimed when the screen is blanked. + * rotation-manager: Don't match orientation when screen is locked. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/914 + * rotation-manager: Fix orientation on unblank when locked. + Changing the configuration when the screen is blanked doesn't do + anything so fix it up on unblank. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/914 + * rotation-manager: Restore transformation on unlock. + Now that we can hit the code path via blank and lock we need + to make sure we don't overwrite the pre-lock transform. + Simplify the code while at that. + * emergency-menu: Improve placeholder page. + Use a dimmed status page when the list is empty. + * emergncy-menu: Set placeholder page to not visible. + When the list is non-empty set the place-holder page as non-visible to + work around https://gitlab.gnome.org/GNOME/libhandy/-/issues/468 + * testlib: Print full socket path. + This makes is simple to spot races due to incomplete test fixtures. + * ticket-box: Switch preferences to native dialog. + This allows the file chooser to use the portal on slightly patched GTK. + * gitignore: Add missing packaging dir + * build: Use 'meson' instead of 'ninja' where suitable + * build: Warn on outdated version. + Warn when we detect versions that trigger less tested code paths because + dependencies are outdated. + * build: Add libfeedback version check. + We want to use newer API + * notifications: Respect per app feedback setting for notifications. + When one changes the feedback profile for an application one expects + notifications sent by that application to adhere to that too. Do that by + using the applications app-id from the notification's app-info when + available. + * ci: Use image with feedbackd 0.2.0 and gtk 4.10 + * ci: Use feedbackd 0.2.0 for packaging build. + We need to pull packages from experimental for that. + * notifications: Drop fallback for older feedbackd. + Distributions wanting to use older feedbackd can revert this commit. + * ci: Reduce package build time. + We're not interested in tests there. + * packaging: Fix package name we look for + * packaging: Don't set build-directory. + This avoids a local package build messing with the default build dir. + * tests: Enhance landscape config. + Add x11 and wayland displays as well so it's ease to reuse to verify + test failures + * notify-manager: Allow to set a timeout on shell notifications + * top-panel: Remove currently unused actions. + What comes back will move to the power-menu. + * top-panel: Close settings before taking screenshot. + Otherwise it won't be very useful. Screenshots of the settings + itself can be made via power-button long press. + * rotation-manager: Simplify debug print. + The values of orientation-lock and claim are known at this point. + * rotation-manager: Match up orientation after unlock. + We claim the accelerometer on unblank and make the orientation match the + sensor readings. However when the device is locked we ignore these + values. For these cases we want to match the orientation again after + unlock. Thanks to Adam Plumb for the suggestion + + [ Sam Hewitt ] + * system-modal-dialog: Add missing style for only-child dialog button. + This avoids having only one corner rounded in dialogues with a single + button. + * data: Add icons for emergency call and screenshot + * styles: Add style for emergency call related buttons + * power-menu: Improve styling + + [ Мирослав Николић ] + * Update Serbian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Aleksandr Melman ] + * Update Russian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Martin ] + * Update Slovenian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Fran Dieguez ] + * Update Galician translation + + -- Guido Günther Mon, 01 May 2023 13:23:40 +0200 + +phosh (0.26.0) experimental; urgency=medium + + [ Guido Günther ] + * ci: Montage all screenshot dirs + * testlib: Prefix test env var with PHOSH_TEST_ + * testlib: Allow to override phoc.ini. + This can be useful for testing different resolutions + * tests: Set test type rather than locale. + This allows us to reuse the tests for other things than + testing different locales + * tests: Screenshot high contrast shell too. + This makes validation simpler. + * tests: Copy keyfile to builddir. + The tests modify the keyfile so in order to avoid an unclean source tree + copy them to the builddir and use it from there. + * tests: Run tests in landscape. + Not all of them make sense as the device rotates to natural orientation + for some of them but let's keep them for now as it would be nice to have + those fixed for use cases like landscape plus keyboard. + * tests/screenshots: Check return value too + * tests: Make sure the keyfile is in place when the tests are run. + Fixes: ce119310013d742762a6e6d1b56322d66a0d49e0 + * plugins/emergency-plugins-prefs: Fix deprecation + gtk_widget_hide is deprecated in GTK 4.9 + * plugins/ticket-box-prefs: Ignore file chooser deprecation. + As it's deprecated since 4.10 and the new GtkFileChooser is only + available in 4.10 let's wait a bit until we move over. + * ci: Use "meson setup" + No functional change, just to avoid the deprecation warning + * phosh-enums: Sort alphabetically + * settings: Drop child properties that don't apply to HdyClamp. + Fixes 394974f0e2fdb729f9772895c183b38dde7e5e79 + * settings: Make quick settings take enough vertical space. + This avoids a scroll bar e.g. when opening settings on the lock screen + when there's enough vertical space. + * plugins/emergency-info-prefs: Mark labels as translatable. + Thanks Piotr Drąg + * main: Don't try to open default display when parsing gtk options. + This makes `phosh --version` work without any compositor or with + unconfigured XDG_RUNTIME_DIR. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/907 + Thanks to Ellie + * rotation-manager: Don't react on power-mode changes. + Since we split locking from blanking acting on lock is enough. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/903 + * wifimanager: Shorten property names. + Matches HACKING.md + * wifimanager: Add property docs. + This makes sure they show up in the generated documentation + * wifimanager: Track whether we're an active wifi hotspot + * Add suspend manager. + This gives us a place where we can put inhibitors. The first inhibitor + prevents suspend when the wifi hotspot is active. + * protocol: Update phosh-private to latest version. + No functional changes + * keyboard-events: Notify key release when using newer protocol version. + Listeners can chose if they care by passing a boolean parameter to the + action. If they don't pass any parameter they only get key pressed. + * screen-saver-manager: Only blank on key release. + This avoids unblanking the screen if the user presses the button for a + longer period of time. It will also allow us to handle long press. + Unblanking already happens on power button press as the activity fed by + the compositor is enough (similar to e.g. the volume buttons). + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/911 + * d/control: Bump phoc dependency. + Phosh will mostly work with earlier versions but 0.26.0 is required + for the keyboard event protocol changes. + + [ Sam Hewitt ] + * css: Fix quick-setting label in high contrast + + [ Newbyte ] + * meson: Use proper SPDX licence identifier. + * README: Use proper SPDX licence identifier. + + [ Aleksandr Melman ] + * Update Russian translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Asier Sarasua Garmendia ] + * Update Basque translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Sabri Ünal ] + * Update Turkish translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Martin ] + * Update Slovenian translation + + [ Nathan Follens ] + * Update Dutch translation + + -- Guido Günther Mon, 27 Mar 2023 11:48:28 +0200 + +phosh (0.25.1) experimental; urgency=medium + + [ Guido Günther ] + * lockscreen-manager: Avoid multiple lockscreens. + When bringing up the lockscreen and shields we might get more events + from the compositor that can trigger locking, which might result in + multiple lock screens. In order to avoid this, guard the locking sequence + and only add more lock screens when done. + This happens during suspend but can also happen when pressing the power + button quickly without any lock delay. (Closes: #838) + * lockscreen: Ref lockscreen during auth. + This makes sure it can't go away while the auth thread is running. + * layer-shell: Use cast. + This gives us proper warnings if something goes wrong + * lockscreen-manager: Improve check. + We can check for the type too + * layersurface: Use consistent logging. + Always use the address of self, using the surface's address just makes + matching harder. Also always put the namespace first. + Doing this in gdb is easier but this makes debugging simple in case one + doesn't have the sources around. + + [ Sam Hewitt ] + * settings: Clamp sliders in quick settings + + -- Guido Günther Thu, 02 Mar 2023 11:01:11 +0100 + +phosh (0.25.0) experimental; urgency=medium + + [ Guido Günther ] + * build: Use gnome post-install when available. + As this is only for dev builds we can assume recent meson so drop + the hand crafted build-aux. + * meson: Use version_compare. + Easier to read + * build: Drop phosh-config.h.in. + This avoids this file getting out of sync with the variables set by + meson. + * util: Use memfd for shared memory on Linux. + This avoids makes shm handling simpler as the compositor doesn't have to + worry about the SIGBUS handler. + I got reminded of this by https://lwn.net/Articles/918106/ + * ticket-box: Drop superfluous '-prefs' in log domain + * build: Add a shared header for prefs plugins. + This is used to pass data for i18n so that we don't have to do it for + every plugin. + * ticket-box: Setup text domain. + We use phosh text domain but treat things as a library so + it can work in e.g. phosh-mobile-settings. + * tools/plugin-prefs-standalone: Fix locale dir in test mode. + The plugins don't know that we're using a prefix for the installed data + in case we're running the tests so fix that up again after the plugin is + loaded (which invokes bindtextdomain() with the unprefixed path). + * postinst: Trigger update notification + * util: Wrap g_clear_fd. + This allows us to use it on older glib too. + * screen-saver-manager: Separate locking from blanking. + Honor org.gnome.desktop.screen-saver lock-enabled so we don't lock the + screen if that is disabled and handle lock-delay to not lock + immediately. + For that we mark screensaver as active/inactive based on the power mode + of the primary monitor. We don't use the built-in monitor since that can + be disabled in multi-screen setups but the primary is the last one to go + off. + * screen-saver-manager: Document what affects blanking and locking + * screen-saver-manager: Handle power button. + This moves it out of the compositor allowing us to distinguish lock + from blank + * gnome-shell-manager: Don't allow to bind power key. + This allows us to do the "blank screen" handling until we fixed gsd#703. + * shell: Track blanked state via primary monitor. + If that is blanked all other outputs should be blanked too + * screen-saver-manager: Unset idle source close to return. + The return value indicates we want to remove the source so reset + the idle_id close to that. + * screen-saver-manager: Inhibit logind actions on power button press. + We want to handle this ourselves. This will hopefully move to g-s-d (see + gsd#703) once there's consensus. + * screen-saver-manager: Disable lock delay timer on unlock + * screen-saver-manager: Disable lock delay timer when screen becomes active + again + * Add a revealer. + Similar to Gtk's revealer but toggles the transition based on the + `present` property which also triggers it's own visibility so we don't + use up any size when the child is hidden. + * top-panel: Use PhoshRevealer for the top bar status icons + * plugins/ticket-box: Exit early if we fail to list the tickets + * plugins/emergency-info-prefs: Setup text domain. + We use phosh text domain but treat things as a library so + it can work in e.g. phosh-mobile-settings. + * plugins/emergency-info-prefs: Use AdwWindowTitle. + Uses the correct style and ellipsizes automatically. + * plugins/emergency-info-prefs: Use a larger default size. + A larger window avoids scrolling and makes things less fiddly to type. + * tests: Screenshot emergency plugin prefs too + * keypad: Drop overly eager debug message. + Just to be on the safe side in case someone has too much + logging enabled. + * thumbnail: Shorten property enum names. + See HACKING.md + * thumbnail: Move property doc to doc string. + We have it documented, make it show up in the docs + * notify-feedback: Handle locked as blanked. + When the screen is locked and something lights it up (or the user is + using a plugin) we want to make sure there's some feedback that a new + event arrived so handle lock as blanked for now. + We'll revisit this when continuing the notification feedback cleanup. + * screen-saver-manager: Split out unarming the lock delay timer. + Move it to a separate function makes things more descriptive + and logging consistent. We'll also get a function for arming + the timer in the next commits. + * screen-saver-manager: Split arming the lock delay timer into separate + function. There are cases where we only want to activate the timer since + the screen is already blanked (e.g. when the compositor initiated the screen + blank). + * screen-saver-manager: Make sure lock timer is enabled on power mode changes. + The compositor can initiate power save. Make sure screen locking is + enabled in this case too. + This can happen via 3rd party tools but also when g-s-d sets the + PowerSaveMode DBus property on phosh's DisplayConfig dbus interface - we + could handle this by letting the PhoshScreenSaverManager listen to + power-save-mode changes on PhoshDisplayManager but since we need to + react to external changes anyway let's use the same code path. + (Closes: #894) + * util: Indicate that get_desktop_app_info_for_app_id can return NULL + * app-tracker: Check for NULL app info. + Related: #865 + * overview: Handle empty carousel. + We use an explicit cast as the signal definition uses an uint. + https://gitlab.gnome.org/GNOME/libhandy/-/merge_requests/841 + (Closes: #865) + * rotate-info: Adjust to current coding style + * rotate-info: Add enabled property. + Only on when rotation sensor is present and not rotation is not locked. + * feedback-info: Add enabled property. + Only off when profile is `silent`. + * wwan-info: Add enabled property + `TRUE` when the hw is enabled. + * quick-setting: Fix indentation + * quick-setting: Shorten enum names. + Match current coding style + * quick-setting: Use current coding style for property + * quick-setting: Don't use underscore in property name + * quick-setting: Add active property. + Set when the quick setting should appear active + * quick-setting: Toggle style class based on status icons enabled state + * top-panel: Simplify drag handle calculation. + Make the bottom bar draggable by default and let PhoshSettings enlarge + it by a given amount (it's drag-handle offset). + * settings: Update drag-handle on size allocation changes. + The allocation changes e.g. between portrait and landscape, + thus we need to update the handle size. + This, together with the previous commit + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/802 + * settings: Reindent ui file. + This was skipped in 66642424a370890430702d65ea8d23be0f258e3b to ease + rebasing. + * settings: Swap clamp and scrolled window. + This makes sure the whole width is scrollable which eases scrolling in + landscape mode as there's no dead area to the left and right. + * util: Detect multi finger touches too. + When checking for a gesture type we need to handle multi finger presses + as otherwise a 2 finger touch will open the panel. (Closes: #853) + + [ Chris Talbot ] + * emerg-info: Factor out common enumerations + * lockscreen-plugins: Add emergency info Preferences + + [ Cédric Bellegarde ] + * screen-saver-manager: Add a suspend delay inhibitor. + We need to be sure ActiveChanged D-Bus signal is emitted before going to + suspend as we'd otherwise blank right after resume. + + [ Sam Hewitt ] + * quick settings: Rework layout and stylesheets for new design + - rework flowbox buttons to appear as toggles + - shuffle around things to wrap entire pulldown with GtkScrolledWindow + - update stylesheet + - change how QS focus + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Martin ] + * Update Slovenian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Hemish ] + * Update Hindi translation + + -- Guido Günther Tue, 28 Feb 2023 13:03:17 +0100 + +phosh (0.24.0) experimental; urgency=medium + + [ Guido Günther ] + * Drop pandoc. + We don't need to convert from markdown anymore + Fixes e29a16ff3e6fb3fcc46ceec4fe44f94b38006063 + * Drop deps on doc packages. + Not needed for gi-docgen + Fixes 89f58c9596950047ce83c5d6ced95d93a484e715 + * docs: Add GTK and Libhandy deps + * notification-content: Check for `NULL` + * notification-content: Check for NULL lookup + g_desktop_app_info_get_string_list can return NULL, check for that. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/874 + * bt-info: Add translation context for "On" + Add a context so "On" can have a context specific translations. + * rotateinfo: Add translation context for "On" and "Off" + Add contexts so "On" and "Off" can have context specific translations. + * feedbackinfo: Add translation context for "On" + Add a context so "On" can have context specific translation. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/880 + * ci: Avoid artifact download for ASAN job. + It's not needed and just wastes bandwitch + * ci: Update images to 2023-01-05. + Update the amd64 images to not waste resources by + package updates. + * feedback-manager: Don't try to register signal if we can't get a DBus proxy. + Otherwise we might crash on startup when feedbackd gave up due to a + broken theme. + * feedback-manager: Shorten property enums. + Consistent with HACKING.md + * feedback-manager: Update prop descriptions. + Use doc strings before adding a new property + * feedback-manager: Init icon_name. + This make sure we always display an icon. + * feedback-manager: Allow to track inited state via present property. + If we fail to init libfeedback there's no point to allow the user to + change feedback settings, etc. Hence export this state via a `present` + property similar to other managers that map to quick settings. + * feedback-info: Init parent class first + * feedback-info: Update indentation. + This is the common style nowadays + * feedback-info: Track feedback-manager's parent property + * settings: Set feedback quick setting sensitive conditionally. + It only makes sense to display the icon when feedback manager is + available. + * dbus/login1: Improve argument name + * util: Add a log domain + * util: Make phosh_find_systemd_session more robust. + Don't rely on the display manager to set the desktop environment as this + currently isn't set by e.g. GDM on Bookworm. Rather use our current pid + or let logind make the pick. + Without this we fail to lock the screen when e.g. suspending on a + laptop's lid close. + * screen-saver-manager: Log if we can't get a session from logind + * screen-saver-manager: Improve debug message. + Let's debug both cases + * ci: Make it simple to just run visual tests. + The screen shot tests take screen shots of the running + shell. Make it easy to just run those. + * shell: Fix typo in comment + * thumbnail: Improve class doc + set_thumbnail must chain up to get the property changed + signal emitted. + * system-modal-dialog: Bind title property in init. + Binding in constructed is too late if we want to set the title + via ui file (as happened for the run command dialog) + * testlib: Add ALT keys to modifiers + * gnome-shell-manager: Use default dbus prefix. + This makes names significantly shorter + * dbus: Drop docbook generation. + We'll switch to markdown once that is in a released glib + version and meson supports it too. + * tests: Screenshot run-command dialog too + * tests: Screenshot the OSD too + * ci: Fetch phoc with working pixman renderer from CI + * tests: Reenable overview screenshots. + With the pixman renderer fixed we can reenable these + * po: Mark wayland session file as translatable. + The "Comment" field can be translated + * wayland-session: Improve comment + * wayland-session: Split list by semicolon. + It's a desktop entry files so separate entries should be semicolon not + colon separated. This shouldn't have any practical consequences. + * packaging: Drop superfluous override + * data: Rename phosh startup script to phosh-session. + This hopefully helps to avoid confusion. + * docs: Add a manpage + * docs: Add a manpage for phosh-session + * Build and install manpages. + This can be toggled via -Dman=false + * tools: Enable flash in screenshot check + * screenshot-manager: Invoke complete area functions. + In case of failure complete the correct functions + * screenshot-manager: Move code to capture all outputs into a separate function. + This will make allow to merge it with the + area capture code. + * screenshot-manager: Use do_screenshot for area capture too + handle_screenshot_area and handle_screenshot were doing + mostly the same thing so let them use the same function. + * screenshot-manager: Use automatic cleanup for frames. + Otherwise we'd leak the frames when we fail to create the + file name. + * screenshot-manager: Emit screen-capture event. + This allows us to trigger proper feedback. + * lockscreen: Strip "figure-space" from am/pm time. + Newer glib uses that instead of ' ' to pad the time. + We can drop the whole `g_strv_length (parts) == 3` part once we require + glib 2.74 at runtime. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/883 + Thanks to Nicholas High for the detailed report and testing + * tests/take-screenshots: Remove unused variable + * tools/plugin-prefs: Don't take all available space. + Let the flowbox expand with it's members + * tools/plugin-prefs: Init gettext + * tools/plugin-prefs: Use actions to activate plugins. + This allows us to add keybindings easily which in turn + allows us to add keyboard shortcuts that can then be used + in the screenshot tests. + * tools: Build plugin-prefs too when building tests. + The screenshot tests will use it + * plugins: Keep plugins in a list. + While phosh itself should never bother about the actual plugins + but infer it automatically at run time it can be useful for + tests and tools to have a static list. + * tools: Don't hardcode plugin list + * tests: Add prefs plugins schema paths. + This is needed if we want to either load the lock screen plugins + or their prefs + * tests/take-screenshots: Run ticket box prefs. + We'll toggle individual pref dialogs in follow up commits + * tests/take-screenshots: Take screenshot of ticket box plugin prefs + * ci: Drop needless fixup. + Fixes 3312583e ("gitlab-ci: No need to forcibly remove lcov anymore") + * lockscreen-manager: Remove unused variable + * polkit: Detect cleanup functions. + Rather than using versions to see if we have cleanup for the polkit + related objects rather check for them. + * rotateinfo: Fix typo in debug message + * head: Fix typo + * build: Avoid relative paths for enums + * Update to gmobile 0.0.1. + This is 0.0.1 plus a few patches to build with older glib and to build + json-glib as a fallback when not present. + We adjust to the new API in the same commit as it's a minor change + and keeps things bisectable. + * layersurface: Update copyright years + * layersurface: Make sure we destroy the layer surface before the wl_surface. + Newer wlroots otherwise treats this as a protocol violation. + Thanks to David Leppla-Weber for proposing a fix and testing. + * layersurface: Use class methods instead of signals for map and realize. + This makes it consistent with how we handle "unmap" now so we don't have + parts using the signal and parts using the class method. + * wl-buffer: Move crucial check out of g_return_val_if_fail() + This can be compiled out and although it's unlikely phosh is built that + way, let's be cautious. + * toplevel-thumbnail: Switch to PhoshWlBuffer. + Avoids some redundancies + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/545 + * overview: Don't leak list + gtk_container_get_children is `transfer container` so we need to free + the list. + * widget-box: Don't leak list + gtk_container_get_children is `transfer container` so we need to free + the list. Prompted by checking all call sites due the previous commit. + * overview: Simplify list handling. + Use automatic cleanup and a local variable. This makes it match other + sites that use gtk_container_get_children. + * network-auth-agent: Be move careful on cancel. + When the request got canceled in dispose we must not access the object + anymore so avoid access to self in that case. + * d/control: Bump dependency on phoc. + This is needed for the screenshot tests + * Revert "ci: Fetch phoc with working pixman renderer from CI" + We can rely on a packaged phoc now + This reverts commit 9830a0c76658733d40e6c9d8c55d72e1f57d77e5. + * ci: Switch package build to Debian bookworm. + PureOS is lagging behind. + * session-manager: Use default DBus prefix. + This makes names significantly shorter + * tests/screenshot: Wait a bit longer for prefs app. + This helps on busy systems. Ideally we'd wait for the toplevel to show + up. + * tests/screenshots: Screenshot more dialogs + See + * build: Use summary() + * tests/screenshots: Clarify comment + * tools/plugin-prefs: Adjust setup when run in the test-suite. + The test-suite installs the locale files in a local directory, + make sure we pick those up. + * tools/app-buttons: Hide close button. Looks odd in screenshots of the running + shell as it has shell styling. + * tests: Don't emit warning when org.a11y.Bus can't be bound. + Otherwise tools like plugin-prefs might emit it, abort and + make tests using them fail. + * app-buttons: Use desktop files with fewer dependencies. + We have these almost installed anyway for the build so use these so we + don't have to pull in additional dependencies just for the icons. + This improves the running-app and overview screenshots in the testsuite. + * build: Avoid wayland protocols and gtk listmodels in docs. + The wayland protocol headers trip up the gir-scanner as the format isn't + compatible. The gtk list models have the wrong prefix. This reduces the + amount of warnings when building docs a whole lot. + * notification: Add some missing transfer annotations + + [ Andrey Skvortsov ] + * lockscreen: Make plugins to use more screen width if available. + It's especially very useful for plugins that display a lot of + information on lockscreen like 'upcoming events' and 'emergency information'. + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Goran Vidović ] + * Update Croatian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Ekaterine Papava ] + * Update Georgian translation + + [ Martin ] + * Update Slovenian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Aleksandr Melman ] + * Update Russian translation + + [ Hemish ] + * Add Hindi translation + + -- Guido Günther Tue, 31 Jan 2023 13:06:29 +0100 + +phosh (0.23.0) experimental; urgency=medium + + [ Evangelos Ribeiro Tzaras ] + * call: Only stop time when call is ended. + We want the timer to continue ticking when on hold to be consistent with + calls. Now we also need to guard against starting the GTimer multiple times. + Originally filed as + https://gitlab.gnome.org/GNOME/calls/-/issues/502 + * plugins/ticket-box: Remove duplicated text. + One warning is good enough + + [ Guido Günther ] + * call: Document class. + This makes it show up properly in the documentation + * sm.puri.OSK0: Document DBus interface. + As other projects sync from here document the interface implemented + by squeekboard, virtboard and phosh-osk-stub better. + * docs: Include the sm.puri.OSK0 DBus interface in the documentation. + This allows us to link to the rendered docs. + * widget-box-standalone: Initialize libhandy. + This makes sure the styles get loaded. + * dir-locals: Prevent tabs in meson mode + * Drop remaining users of config.h. + It was renamed to phosh-config.h a while back + Fixes: b5b03b0c0123e8533896cddbfff19f1648f77d61 + * widget-box-standalone: Improve comment + * widget-box: Add missing quote + * build: Add a pkgconfig file with plugin information. + This allows other tools and external plugins to figure out + the file system locations. + * build: Allow to disable the plugin build. + This allows to disable all plugins (e.g. to reduce compilation time + or dependencies). + * build: Add and install plugin header file. + This simplifies external plugins. + * plugins/calendar: Use plugin header + * plugins/calendar: Pass plugin name to compilation units. + This makes sure they don't get out of sync + * plugins/ticket-box: Use plugin header + * plugins/ticket-box: Pass plugin name to compilation units. + This makes sure they don't get out of sync + * plugins/upcoming-events: Use plugin header + * plugins/upcoming-events: Pass plugin name to compilation units. + This makes sure they don't get out of sync + * Add a phosh-dev package. + This includes pkg config files and headers. Currently this affects + plugins and is useful for locating the plugins (e.g. to load them or + parse related information) or to build plugins outside of phosh's source + tree. + * shell: Allow to query blanked state easily + * notifications: Use silent feedback when blanked, not locked. + Now that these two states will become different. + * shell: Disconnect the right monitor in phosh_shell_set_primary_monitor. + Fixes 817ac486 ("shell: Update top panel height on primary monitor resize") + * shell: Simply on_monitor_removed logic + * build: Add include directory for dbus sources too + * Switch to gi-docgen (Closes: #589) + * src: Fix doc headers. + Drop short_description and title so gi-docgen can pick them up. + * debian: Adjust packaging for gi-docgen switch + * ci: Adjust to new doc location. + We also skip the doc check until we fixed more warnings. + * tools: Update doc check. + Drop warnings/errors from wayland protocols and contrib source code. We + always exit with success for the moment until we cleaned up more bits. + * wwan/ofono: Fix comment. + Don't start with `**` if not meant for gtk-doc/gi-docgen + * wwan/mm: Fix comment. + Don't start with `**` if not meant for gtk-doc/gi-docgen + * vpn-info: Fix property name + * docs: Mention sm.puri.OSK0. + The docs are generated via our patch to gdbus-codegen. Should these land + we can create it during build time with the next glib release. + * docs: Remove now unused files + * network-auth-prompt: Document signal arguments + * polkit-auth-promopt: Document signal arguments + * ci: Skip pages job with PKG_ONLY + + [ Chris Talbot ] + * lockscreen-plugins-calendar: Encapsulate calendar to include ui file + * lockscreen-plugins-calendar: Encapsulate in GtkBox + * lockscreen-plugins-calendar: Add CSS + * lockscreen-plugins: Create Emergency Info and Contact Plugin + + [ Aleksandr Melman ] + * Update Russian translation + + [ Zurab Kargareteli ] + * Update Georgian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Martin ] + * Update Slovenian translation + + -- Guido Günther Tue, 20 Dec 2022 16:23:55 +0100 + +phosh (0.22.0) experimental; urgency=medium + + [ Guido Günther ] + * build: Use glib_ver consistently + * build: Bump required glib version to 2.66. + This is the version in Debian stable so we can safely + use this as baseline. + * Allow for notifications to have actions on the lockscreen. + Related: https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/103 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/620 + * Add libgmobile subproject. + We'll use it for timers in follow up commits + * upcoming-events: Use a suspend robust timer. + We need a timer source that takes system suspend into account. + Use gmobile's gm_timeout_add_seconds_once() that is there for + that purpose. (Closes: #826) + * lockscreen: Use G_SOURCE_{REMOVE,CONTINUE} + Easier to read than TRUE/FALSE. + * lockscreen: Use g_clear_handle_id() + Less code, less chance to break things on refactor. + * lockscreen: Don't use two independent if clauses. + It's all based on index so use `if/else` instead. + * lockscreen: Set entry sensitive when focused. + Focusing will fail otherwise so let's keep these together + * lockscreen: Mark entry as initially sensitive. + This avoids the letter one on the keypad being focused initially + fixing a visual glitch that was triggered by unifying button styles: + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1154 Tested + with both phosh-osk-stub and squeekboard. + + [ codewood ] + * batteryinfo: Use percentage instead of icon-name to determine icon. + Icon themes like adwaita provide icons for 10% steps, use these based on + the current battery level rather than just the 3 levels provided by + upower via the icon-name property. + + [ Sam Hewitt ] + * overview: Close button improvements + - new more proportional close icon + - remove redundant css from close button and activity widget + * style: Unified the button styles + - cleans up a lot of duplication of button css + - makes a single style for all buttons + + [ Zurab Kargareteli ] + * Update Georgian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Vojtěch Vengrin ] + * Update Czech translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Danial Behzadi ] + * Update Persian translation + + -- Guido Günther Wed, 26 Oct 2022 16:26:10 +0200 + +phosh (0.21.1) experimental; urgency=medium + + [ Guido Günther ] + * screen-saver-manager: Shorten DBus names. + Make it match the PhoshDBusLoginSession proxy. + * screensaver-manager: Use cancellable. + This makes sure we cancel pending calls on shutdown and allows + us to drop some refs/unrefs. + * packaging: Ship NEWS. + It's a short summary of upstream changes. + * packaging: Update copyright. + This got improved quite a bit so lets fetch this upstream too + to minimize the delta. + * packaging: Fix postinst interpreter + * event-list: Use plain week day. + This matches the designs and helps languages that would otherwise + need a more complex pattern. + * upcoming-events: Use ngettext for different plural forms + * Update libcall-ui to 0.0.5. + There aren't any breaking changes but the style refresh + makes longer strings fit way better. + * log: Track log domains instead of passing it to the handler as user data. + * ci: Update cross image. This allows the i386 build to pass again + * screen-saver-manager: Shorten DBus names. + * shell: Avoid screen-saver singleton. + * monitor-manager: Add method to set power-save mode of all monitors + * Move blank on presence status change to screen-saver. + This was on the LockscreenManager before but for blanking + only we want it on the screensaver. + + [ Evangelos Ribeiro Tzaras ] + * packaging: Mark phosh-doc as Multi-Arch:foreign (was all) + * log: Guard against setting g_log_set_writer_func() multiple times. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/841 + * shell: Tweak phosh_shell_set_state() debug message + + [ Fiona Klute ] + * util: Don't use G_REGEX_JAVASCRIPT_COMPAT. + G_REGEX_JAVASCRIPT_COMPAT is unsupported as of glib 2.74 and + not needed for the regex at hand. + This works around a crash triggered by glib's switch to pcre2. + See https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2920 + + [ Sam Hewitt ] + * style: Overview & appgrid fixes for high contrast + + [ Zurab Kargareteli ] + * Update Georgian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Goran Vidović ] + * Update Croatian translation + + [ Aleksandr Melman ] + * Update Russian translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Марко Костић ] + * Update Serbian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Rūdolfs Mazurs ] + * Add Latvian translation + + [ Martin ] + * Update Slovenian translation + + -- Guido Günther Mon, 26 Sep 2022 11:19:53 +0200 + +phosh (0.21.0) experimental; urgency=medium + + [ Evangelos Ribeiro Tzaras ] + * Update libcall-ui. + This brings some UX improvements for the call display and updated + translations. + + [ Guido Günther ] + * shell-manager: Simplify include + * shell-manager: Report version over DBus. + Use GNOME_PLATFORM_VERSION and phosh's version on recent GNOME and + fall back to gnome-desktop XML for older versions. + Thanks to Jeremy Bicha for bringing this up. + * gitlab-ci: Update container. + Drop several workarounds to fetch recent phoc and just get it from + unstable until it migrates to testing. + * ambient: Set array size to 0. + A bit simpler than removing elements + * ambient: Clear array when we unclaim the sensor. + This makes sure we don't start with a partially filled array + claiming it again + * ambient: Make sure to stop callback and unref array. + Otherwise we leak on shell shutdown. + * ambient: Claim / unclaim the sensor on shell unblank / blank. + This makes sure the sensor chip can sleep when the screen is off + (and hence any theme switching is useless) + * screenshot-manager: Capture all outputs. + So far we only grabbed the primary one. + * sceenshot-manager: Handle grabbing a screen area. + This allows the "Selection" button in gnome-screenshot to work. + We use the slurp tool for that. + * d/control: Recommend slurp. + Used for marking the screenshot area + * tools: Add helper to take screenshots + * screencopy-manager: Avoid stdout. + This trips up alpine's compiler + * screenshot-manager: Fall back to g_memdup for older glib. + Can be removed once we can assume 2.68. + * libcall-ui: Update libcall-ui. + This unbreaks the lock screen by moving to a commit + that includes + https://gitlab.gnome.org/World/Phosh/libcall-ui/-/merge_requests/55 + * gitignore: Ignore packaging directories + * build: Drop -DHAVE_CONFIG.H. + Fixes b5b03b0c0 ("build: Rename config.h to phosh-config.h") + * build/gtk-list-models: Make them available in other dirs. + Use file() rather than relative paths and introduce variable + for include directories. + * build: Move app id to phosh-config.h. + This makes it available for other services as well + * ci: Update build-deps. + Update the build deps for Debian builds so we catch new ones added in + MRs. + Things got a bit simpler now that recent phoc is in bookworm. + * ci: Add eds-dev to alpine build-deps + * Add gnome-shell's calendar server. + Import the whole folder as of 2c89b62247cf65a806173c901334e18671f67440 + before making any changes. + * calendar-server: Drop unused files + * calendar-server: Fix build + * calendar-server: Don't shadow local variable + * calendar-server: Don't hardcode either phosh nor gnome-shell. + Will hopefully allow to share the code in the future. + * calendar-server: Handle NULL zone + * calendar-server: Use SPDX-License-Identifier + * calendar-server: Add color to events. + We add them as additional hint to each event. Tracking color + changes will be a follow up. + * debian: Install calendar-server and it's dependencies + * plugins: Add an upcoming events plugin. + The upcoming-events plugin implements the phosh-lockscreen-widget + extension point so it can be displayed on the lock screen. It shows the + upcoming events during the next 7 days. Includes style fixes by Sam + Hewitt + * tools: Enable upcoming-events plugin + * tools: Don't use magic constant gtk style priority + * main: Drop unused variable + * ci: Exclude autogenerated and imported sources + * plugin-loader: Add and use property getters. + Eases adding tests + * tests: Test plugin loader. + We skip that under ASAN as plugins aren't found in the ASAN build. + * ci: Drop Needs: for po and license checks. + They don't need to wait for a build. This gives gitlab-ci more + room to schedule jobs as it sees fit. + * ci: Avoid artifact downloads for jobs that don't need it. + This saves ci resources. + Fixes: f4b8daec ("ci: Drop Needs: for po and license checks") + * calendar: Add plugin information. + This allows us to provide description, etc. Since we use keyfiles we can + use the same keys as in desktop files to leverage the translation + system. + * debian: Install plugin info + * upcoming-events: Drop underscore from plugin path. + This makes it match the name. + * upcoming-events: Add plugin information + * widget-box: Don't load widgets twice + phosh_widget_box_set_plugins() already loads them. + * widget-box: Add a placeholder page when a plugin could not be found + * widget-box: Drop define from printf + * tools: Add a nonexistent plugin. + This allows to easily check the missing plugin page + + [ Sebastian Krzyszkowiak ] + * overview: Only focus an activity on page change if it's not focused. + Fixes #818 + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Goran Vidović ] + * Update Croatian translation + + -- Guido Günther Wed, 31 Aug 2022 11:33:08 +0200 + +phosh (0.20.0) experimental; urgency=medium + + [ Guido Günther ] + * media-player: Don't forget to clear cancellable. + We did cancel it but didn't dispose it yet. + * Switch to HighContrast based on ambient sensor readings. + Add an automatic high contrast toggle that allows to switch + to HC automatically based on the current light level. + * settings: Drop superfluous can-focus properties + * shell: Move style sheet getter to separate function. + * tests: Add simple test to load stylesheet. + Stylesheet changes trigger warnings ever so often, add a test + to catch these early. + * d/control: Bump phoc dependency. + Needed for + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/1115 + (Handle draggable-layer sizing on monitor mode changes) + * home: Focus app search on application-view action. + The application-view action (triggered by + KEYBINDING_KEY_TOGGLE_APPLICATION_VIEW (super-a)) is supposed to focus + the app search. This got broken when moving to draggable layer surfaces. + Fixes: 22b03f7f2 ("home: Make it a draggable layer surface") + + [ Sebastian Krzyszkowiak ] + * lockscreen: Use page-changed signal when appropriate + notify::position sometimes ends up sending starting event with position + of the page that's being switched away from, which made us execute handlers + that should only be called when switching into a new page. Fix that by + using page-changed signal when appropriate - timer handling still uses + notify::position since it needs to react to scrolling just being started. + Fixes #664 + * lockscreen: Allow show_unlock_page to execute mid-transition. + Otherwise it feels like the key event is missed when the user starts typing + while page switch is still being animated. + * lockscreen: Clear input right away when pressing ESC. + It gets cleared by page-changed handler, but that happens only at the end + of the scroll animation. If the user presses ESC and then quickly starts + to type again, we would counter-intuitively end up appending to already + filled up entry. + + [ Sam Hewitt ] + * style: Fix osd using hardcoded colors + * style: Fix notifications in high contrast + + -- Guido Günther Wed, 03 Aug 2022 12:32:34 +0200 + +phosh (0.20.0~beta3) experimental; urgency=medium + + [ Guido Günther ] + * lockscreen: Drop am/pm from time formats (Closes: #103) + * data: Install and add icon to desktop file. + This will allow us to use it in e.g. phosh-mobile-settings panels. + * tests: Allow for phosh's icon. + * build: Adjust to polkit version changes. + * build: Rename config.h to phosh-config.h. + Otherwise we end up including config.h from elsewhere e.g. gvc. + * testlib: Kill compositor on SIG{ABRT,SEGV,TRAP} + * drag-surface: Add threshold getter. For symmetrie with the setter. + * drag-surface: Remove drag_surface_new (). The created object is not very + useful as we need to setup layer-surface too. + * drag-surface: Fix argument type. Fold margins are integers. + * tests: Add drag surface test. + This way we at test the setters and getters. + Fixes: 578a666f7fb69553bdf82ff8ab390467ab25165f + * tests: Don't change signal handlers when ASAN is in use. + ASAN wants to tweak these so don't touch them in these cases. + * build: Abort post-install on missing arguments. + * gitignore: Ignore i18n related generated files + * lockscreen: Avoid some keyboard filtering when not on unlock page. + This allows for keyboard navigation with the upcoming lockscreen plugins + and emergency call support. + * Add widget box to lock screen. + The widget box loads plugins that implement the phosh-lockscreen-widget + extension point and puts them on the lock screen. + Swiping to the left switches to the plugins if at least one is enabled. + * build: Generate module cache on install + * tools: Add standalone widget-box. + This allows to test the lock screen plugins without + running phosh itself. + * plugins: Add minimalistic calendar plugin. + It implements the phosh-lockscreen-widget extension point. It's + main purpose is to have a minimalistic example. + * debian: Install plugins. + We use an extra binary package so people can easily get rid of + plugins and their dependencies. It will also help splitting things + into a separate repo later. + * debian: Add trigger for plugin directory. + Run gio-querymodules to build the cache. Most of this copied + from Debian's glib package. + * monitor-manager: Drop useless ref. + We unref right away anyway (by automatic cleanup) at the end of the + function + * monitor-manager: Make different enum types explicit. + PhoshMonitorTransform and wl_output_transform have the same values + but don't convert implicitly. + * monitor: Add helper to detect "tilted" transforms. + We consider a transform to tilt the display if it changes the + display orientation from portrait to landscape or vice versa. + * head: Make header private. + PhoshHead should only be used by PhoshMonitorManager (and tests). We + had it in the comments, reflect it in the file name too. + * monitor-manager: Move transform setting to PhoshHead. + It only affects the heads which will ease testing. + * monitor-manager: Adjust layout when rotating an output. + When rotating an output we need to adjust outputs left and below the + rotated output to avoids gaps or overlaps in the layout. + We shift all these heads whether they're currently enabled or not. + + [ Sam Hewitt ] + * icons: Add symbolic icon for Phosh + + [ Sebastian Krzyszkowiak ] + * tests/stubs: Add phosh_shell_get_docked + * overview: Raise toplevels on activity scrolling. + * shell: Don't fold the top panel and keyboard on unfolding home screen. + This isn't necessary anymore. + * ui/lockscreen: Set the PIN entry to be initially insensitive. + We change sensitivity based on carousel page in C code, but + the initial state stayed out of sync, which could cause + the keyboard to pop up right after locking the screen. + Fixes: 5300ba2ce ("lockscreen: Use entry focus to request OSK") + * stylesheet: Remove background from keypad's focused buttons. + Focus doesn't do much there as the lockscreen intercepts keyboard + input by itself, so don't display any visible focus indication + to not confuse users. + * lockscreen: Focus PIN entry on carousel page change when OSK is on. + * lockscreen: Focus PIN entry regardless of OSK status. + Won't hurt, but seems like a good idea that may help accessibility tools. + * stylesheet: Apply lockscreen PIN entry styles also to disabled state. + Otherwise caret-color may end up being animated when toggling sensitivity. + * home: Remove duplicated "unfold on no activities" handling. + It's already taken care of by PhoshShell. + * shell: Only fold the home screen when the first toplevel is added. + Folding when any window appears can get annoying if the user unfolds + the home screen while waiting for an app to launch (for example to launch + another app simultaneously). + * home: Don't (un)fold while being dragged. + Don't forcefully pull the surface out from under the user's finger. + * overview: Focus the activity on carousel page change. + Without this, we may end up scrolling back to some previously + focused activity on carousel focus if the active activity has + changed while the carousel wasn't focused. + * overview: Focus the activity right away when scrolling to it. + This fixes the same issue as the previous commit, but in cases + where scrolling has been invoked programmatically by phosh and + the animation hasn't finished before PhoshHome gained focus. + + [ Alexander Mikhaylenko ] + * docked-manager: Update gtk4 is-phone setting too. + Since gtk4 needs to be parallel-installable with gtk3, it ships its own + schema. Change it as well. + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Мирослав Николић ] + * Update Serbian translation + + [ Zurab Kargareteli ] + * Update Georgian translation + + -- Guido Günther Tue, 19 Jul 2022 09:52:36 +0200 + +phosh (0.20.0~beta2) experimental; urgency=medium + + [ Guido Günther ] + * screen-saver-manager: Lock screen when suspending. + This avoids seeing the unlocked screen for a moment on resume. + * run: Add --args when using gdb. + This makes sure `-U` isn't parsed by gdb. + Thanks to Adrien Plazas for pointing this out. + * shell: Honor modal dialogs and full screen when raising top-bar. + Just handling the fader isn't enough as we want full screen windows and + modal dialogs to stretch over the top bar as well. + We have some more cases like screenshost screen flash but that needs + proper z-level which we can introduce once the layer-shell-effects is + in. Same if we want system modal dialogs on the lock screen. + Fixes: 55bbe5758ba947a1d77c4b75d4424ea8cf4ebac6 + Closes: https://gitlab.gnome.org/World/Phosh/phoc/-/merge_requests/370 + * shell: Don't try to close top-panel when not yet created. + On start up there's no top-panel so don't try to close it + * shell: Select top-panel layer based on lock state. + When the panel gets created we want the overlay layer in case the screen + is locked, otherwise top layer. + * idle-manager: Drop primary monitor. + We don't need it there and just hold a ref that we (so far) never gave + up again. + * tools: Add helper to submit a notification. + Makes it simpler to test notifications style from the command line. + * data: Indicate that we provide user feedback via audio, led and haptic. + This will help setting apps to configure this. + + [ Evangelos Ribeiro Tzaras ] + * Update libcall-ui to v0.0.3. + Brings some UI tweaks, encryption indicator that is bound to calls + `encrypted` property, fixes a small issue with libcallaudio usage and + lots of updated or added translations. + + [ Linus Walleij ] + * shell: Set monitor when creating rotation manager. + I had an annoying bug on Samsung phones such as GT-I9070, + GT-I8160 and GT-I8530 all using DPI displays that did not + properly instantiate the rotation manager, while + phones on the same platform using DSI would work fine. + If the primary monitor has already arrived when the rotation + manager is instantiated, the monitor does not get assigned to + the rotation manager. + This happens of phones such as Samsung GT-I9070 which use + DPI displays on MCDE that arrive apparently very quickly. + The same U8500 platforms with DSI displays does not have this + problem, presumably because the primary monitor arrives + later. + This fix was developed working entirely on the target, + compiling Phoc and Phosh natively on the GT-I9070 dual Cortex-A9 + and iteratively editing, recompiling and testing modifications + to Phosh on the target itself. + + [ Sebastian Krzyszkowiak ] + * activity: Emit "resized" signal + size-allocate can fire despite of allocation not changing at all, + so add our own signal that checks forchanges to get rid of + extraneous thumbnail refreshes. + * util: Add phosh_util_toggle_style_class + * activity: Add "fullscreen" property + * toplevel: Add "fullscreen" property + * overview: Don't draw grey background around fullscreen window's thumbnail + * top-panel: Crossfade between fold states + * lockscreen: Use entry focus to request OSK. + This allows the compositor to make informed decisions about + when to elevate the keyboard surface above shell. + * home: Reset overview at the beginning of unfolding. + This prevents flicker at the end of unfolding animation. + * overview: Request a thumbnail for foreground activity on reset. + This reduces the amount of flickering when the thumbnail gets + updated, especially when OSK hiding is involved. + * overview: Don't update thumbnails when overview is unfolded. + We don't do live updates of thumbnails yet, so having them update out of + sudden but only in some situations makes it look unpolished. We still want + to update when overview is folded in order to keep cache of thumbnails + in state that was seen by the user the last time when windows change focus. + * home: Update keyboard interactivity right away when user starts folding. + This gets rid of keyboard flicker when folding animation finishes and + an application that requested input method gets focus back. + * home: Use a revealer to fade in/out the keyboard button + * home: Hide the keyboard button right away when starting dragging + * home: Don't hide the arrow when the surface is being dragged. + Without this check, the arrow stays hidden during folding with + no activities until the end of that animation. + * top-panel: Disable keyboard interactivity while dragged. + This allows OSK to pop back up earlier when folding, and makes it stay + visible during unfolding animation or drag. + + [ Pablo Barciela ] + * app-auth-prompt: Drop unused code + add_choice_to_gvariant looks at the switch state not at data + we store on the GObject so this code can go. + + [ Gerben Jan Dijkman ] + * polkit-auth-agent: Scope cleanup function for PolkitAgentListener. + Newer polkit defines a cleanup function so avoid it for polkit >= 0.121 + + [ Sam Hewitt ] + * notifications: Layout adjustments + * put buttons in a horizontal alignment + * update some label style classes + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Anders Jonsson ] + * Update Swedish translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Jürgen Benvenuti ] + * Update German translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Aleksandr Melman ] + * Update Russian translation + + -- Guido Günther Wed, 15 Jun 2022 14:10:41 +0200 + +phosh (0.20.0~beta1) experimental; urgency=medium + + [ Guido Günther ] + * This is a beta release + * portal-access-manager Sync dialog visibility with locked state. + Otherwise we can have the dialog pop up on the lock screen. + * settings: Make ui file match the class name. + Another ancient inconsistency cleaned up. + * Add no-notifications icon + * settings: Mark button label as translatable + * settings: Add indication when there are no notifications + (Closes: #744) + * lockscreen: Fix typo + * lockscreen: Remove unneeded packaging + * lockscreen: Name clock and date labels. + Rather than using a style class name this single instance. + This allows us to attach style classes in less confusing ways. + * lockscreen: Move call-ui down by top-panel's height. + Since the top-bar now takes up space we don't want the back + button to overlap the top-bar. + We chose that approach instead of changing the lockscreens exclusive + zone so that it keeps to extend below the top bar. This will make e.g. a + transparent top bar work as the background can shine through and also + make the "centered" property of GtkBoxes on the lock screen still center + on the screen without having to care for additional padding. + * top-panel: Move top-panel to overlay layer. + This allows to show the indicators on the lock screen. + * top-panel: Prevent unfold when locked. + We'll enable quick settings access in a separate MR. This will be a bit + more involved as we need to disable long press, hide notifications, etc. + * top-panel: Hide clock when shell is locked. + We have a clock on the lock screen already + * lockscreen: Drop indicators + * lockscreen: Adjust clock and data size based on notifications. + If notifications are present use a smaller clock. (Closes: #640) + * lockscreen: Add bottom margin to center clock and date + * lockscreen: Use revealer to show/hide notifications + * shell: Move lock setting to function. + This way we can avoid setting it multiple times + * shell: Use explicit notify for locked property. + Avoids rerunning code when there aren't any changes. + * proximity: Add property to track fader status. + This allows other parts of the shell to perform actions based on + that. + We want to move the whole fader handling out of the proximity sensor at + some point. + * shell: Hide top-panel when proximity fader is on. + Since the top-panel uses an exclusive zone and the fader doesn't, the + top-panel is arranged on top of the fader. Due to this the top-panel + would still accept input e.g. during phone calls which leads to odd + results when typing with the ear. + So hide the top-panel for the moment when the fader is active. Later on + we can move it one layer down which is a bit nicer but needs the + corresponding phoc patches to support that. + * shell: Mirror docked property. + Easier to access (read) in other places of the shell + * shell: Document `locked` property + * shell: Use getter for locked property. + For consistency. + * top-panel: Disconnect seat signals. + Otherwise we might get called after dispose (e.g. after moving + primary to a new output) + * top-panel: Show keyboard indicator only in docked mode (Closes: #206) + * top-panel: Remove packed from indicators box. + Not needed there. + * top-panel: Remove can-focus from indicators box. + Not needed there. + * batteryinfo: Fix indentation + * feedbackinfo: Add muted property. + This allows to hide the icon when 'full' profile is in effect. + * top-panel: Show feedback indicator (Closes: #284) + * util: Use GCC diagnostic push. + This makes sure we restore the old value + * settings: Add on-lockscreen property. + Indicates whether settings are currently displayed on the lock screen + or in the unlocked shell. + This eases binding bindings in templates. + * settings: Add a box for the elements not visible on the lock screen. + Toggle these by the "on-lockscreen" property. + * settings: Consolidate all g-c-c panel opening in a single location + * settings: Ignore long press that leads to g-c-c. + This won't work on lock screen. We can keep the long press on + the rotation button as this doesn't spawn anything else. + * settings: Reindent ui file. + Do this as a separate commit to ease review and rebase + * top-panel: Hide power button on lock screen. + We might support that later on but just hide it for now. This will + change anyway with gestures. + * Revert "top-panel: Prevent unfold when locked" + Let's allow for quick settings on the lock screen + This reverts commit a5beb47745a8632ec3bab3377005a68b009ef2b4. + (Closes: #603) + * top-panel: Move power button. + Move the power button out of the stack that has the size + of the top-bar. This will allow us to style it properly. + * top-panel: Style power button. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/331 + * top-panel: Add larger clock when panel is unfolded. + See e.g. https://gitlab.gnome.org/World/Phosh/phosh/-/issues/331 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/717 + * top-panel: Use property binding for the clock. + Less code. + * top-panel: Add lock-screen button. + See https://gitlab.gnome.org/World/Phosh/phosh/-/issues/331 + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/331 + * top-panel: Add arrow. + This makes it match the mockups more + * top-panel: Disable power/user button when on lockscreen. + We do this by introducing a "on-lockscreen" property. + * top-panel: Remove from network box. + Just use the defaults. + We don't have outstanding changes there so it doesn't create any merge + conflicts. + * d/control: Add libxml-utils. This allows to strip blanks. + * d/control: Add xwayland to test dependencies + * d/control: Add librsvg2-common. + This helps to load SVG icons in the tests + * tests: Disable thumbnail related test. + Phoc currently doesn't support this with the headless backend. + See https://gitlab.gnome.org/World/Phosh/phoc/-/issues/262 + * gitlab-ci: Only run unit test suite on alpine. + This is more future proof than just skipping the screenshot tests + * gitlab-ci: Build container without cache. + This makes sure the container is rebuilt instead of just refreshed. + * gitlab-ci: Use more caps when running docker container. + This allows us to run gdb, etc. and we mostly want it for debugging. + * gitlab-ci: Use newer docker file for builds and tests. + This makes sure we build against GNOME 42. + * layer-surface: Use namespace in debug messages. + This greatly simplifies figuring out which surface this is about. + * shell: Document phosh_get_usable_area () + * shell: Add helper to get whole screen area + * top-panel: Fix namespace + * top-panel: Use the full screen height for the top panel. + For that we reduce the notification's scrolled window minimum height a + bit as it will expand by itself. + This prevents the settings menu from expanding beyond the lower screen + edge in portrait mode (which happened so far when media player and + notifications were shown). + Using the fulls screen height is also in line with the designs - + although it will be a bit black heavy in docked mode. We will need to + compensate for that e.g. by using less black (and e.g. just blur the + underlying screen space). + This is in preparation for the gesture code as the draggable surface also + uses the full screen height. + * settings: Set vexpand on the notifications list box. + Otherwise the initial notification might get cut off if it has multiple + lines and an action. + * top-panel: Ease UI file navigation. + Add some comments + * top-panel: Fix box_clock indentation. + When moving elements around we wanted to have as little diff noise as + possible, fix that up now. + * proximity: Move fader handling to separate functions. + Unclutters the code but also makes sure we always emit the notify + signal. This wasn't the case in on_proximity_claimed before which + resulted in the top-bar not showing up again. + * meson: Add default prefix + * dbus: Use default prefix for org.freedesktop.hostname1 + * dbus: Use default prefix for org.freedesktop.login1 + * top-panel: Fix inverted on-lockscreen logic + * top-panel: Hide lock on lock-screen + * gitlab-ci: Pull phoc 0.13.1 from Debian sid. + We need that for the tests to pass + * d/control: Require recent enough phoc. + We want one that has layer-surface v2 support + * protocol: Update layer-surface protocol to version 4. + This is from phoc as of commit e86771e1ffccf216b2c016cf93226cc77e5caab5 + * wayland: Require layer-shell protocol version 2. + We want to use set_layer. + * layersurface: Allow to set layer + * shell: Lower top-panel when proximity fader is on. + Since the top-panel uses an exclusive zone and the fader doesn't the + top-panel is arranged on top of the fader. Due to this the top-panel + would still accept input e.g. during phone calls which leads to odd + results when typing with the ear. + Moving the top-panel to another layer is a bit nicer than just hiding + it since we still have the layer around on the unlocked shell in + case something goes wrong. + * gitlab-ci: Deduplicate locale generation + * home: Fix typo + * settings: Fix typo + * top-panel: Update doc string + * shell: Drop outdated comment + * build: Untabify tests meson build file + * overview: Drop properties at their default values + * layer-surface: Drop constructed() + We can do all the work in init() which is there anyway. + * layer-surface: Only emit configured when s.th. changed + * Add draggable layer surfaces. + They use the layer-shell-effects protocol to mark themselves draggable in + one direction. + Implementation is similar to layer-surface: + - configure on mapped + - guard all property setters so they don't fail with a NULL drag_surface + which is the case during object creation + * phosh-wayland: Require layer-shell-effects. + We can't do anything useful without it. + * phosh-wayland: Make it more clear what's missing + * util: Add helper to distinguish touch from non touch press/release + * top-panel: Remove superfluous can-focus properties from network box + * top-panel: Make it a draggable layer-surface. + Turn the top-panel into a draggable layer surface. This changes the + behaviour from hiding/showing certain elements based on folded/unfoleded + state to having all elements visible all the time having the DragSurface + determine which bits are currently on screen. + Since the DragSurfaces has folded and unfolded states by itself we can + use that instead of keeping track of it on our own. + * top-panel: Use full screen height. + * top-panel: Don't trigger feedback. + We don't want haptic feedback on swipe start and not on click either + as that will be mouse only. + * top-panel: Animate arrow + * top-panel: Handle mouse click via gesture. + We don't want to trigger fold/unfold on touch events as they'll + be used for drag. + This allows us to drop the button completely. + * settings: Allow to retrieve a suitable drag handle offset. + Based on the UI elements calculate a suitable draggable area. + In follow ups we can take the notifications scroll position + into account as well. + * top-panel: Update drag-handle based on settings. + Respect the drag area for touches settings informs us about. + * home: Make it a draggable layer surface. + Disable click related handling for now. Will be added + back in a later commit. + * home: Don't trigger feedback. + We don't want haptic feedback on swipe start and not on click either + as that will be mouse only. + * home: Drop some unneeded properties. + Eases GTK4 migration + * home: Handle mouse click via gesture. + We don't want to trigger fold/unfold on touch events as they'll + be used for drag. + Similar to what we do for the top-bar. + * settings: Don't close when clearing all notifications. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/760 + * tests: Disable parallel tests with compositor. + They currently fail when run in parallel. We'll investigate when phoc + side is merged. Tests don't run much longer as those are quick. + * gitlab-ci: Use a compositor that can handle gestures. + Workaround the fact that the phoc side isn't merged yet by grabbing + a suitable package from CI. + * top-panel: Reindent ui file. + Mechanical change now that + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/934 is + merged. + * home: Reindent ui file. + Mechanical change now that + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/934 is + merged. + * bars: Add back 1px border in high contrast. + Fixes 0ea0cccfd ("home: Handle mouse click via gesture") + * top-panel: Drop phosh-top-bar style class. + We have CSS by widget class and name already , we don't need styling per + class too. + * top-panel: Emit activated signal instead of folding/unfolding. + Don't frees the top-panel from managing it's own state again. + While at that make the signal and method names match our top-panel + naming. + Fixes 0ea0cccfd ("home: Handle mouse click via gesture") + * top-panel: Fix comment + * shell: Rename panel to top_panel. + The "panel" term is from the times when we only had a single + panel at the top but no home panel. + * notify-manager: Move source layout closer to guidelines. + We want the signals above the definition and the forward declaration + for interface right above G_DEFINE_TYPE_WITH_CODE(). + * notify-manager: Drop SIGNAL_ from signal names. + Consistent with other source files. + * notify-manager: Emit "notification-activated" + Emit a signal when a notification gets activated. This allows other + parts of the shell to react on this. + * shell: Fold top-panel on screen lock. + This avoids that the user doesn't look at the (almost empty) settings + when unblanking the screen. + * shell: Fold top-panel when a notification gets activated. + Otherwise we still cover the whole screen + * util: Add helper to check if gnome-software is available + * app-grid-button: Add "View Details" entry. + Helps https://gitlab.gnome.org/World/Phosh/phosh/-/issues/549 as + gnome-software offers an uninstall button there. + * d/control: Bump phoc dependency. + We require at least 0.20.0 due to phoc-layer-shell-effects + * gitlab-ci: Use phoc 0.20.0. + We'll update the base images once it migrated + + [ Sebastian Krzyszkowiak ] + * hks-manager: Set rfkill IOChannel to non-buffered. + Part of the kernel API is that the messages are delivered as + separate reads. Buffering can mess it up, so disable it. + This has been made apparent by kernel commit + 54f586a9153201c6cff55e1f561990c78bd99aa7, + which makes the reads not exceed RFKILL_EVENT_SIZE_V1 by default. + We are asking for more, so buffering was doing additional + read calls on our behalf. + * top-panel: Set drag threshold to 0.3 for consistency with PhoshHome + * top-panel: Update PhoshArrow progress during surface drag + * home: Update PhoshArrow progress during surface drag + + [ Arnaud Ferraris ] + * phoc.ini: Drop no longer needed scale config. + As `phoc` is now able to automatically compute the scaling factor based + on the output's physical size, we no longer need to set this value in + the default config: `DSI` output should return a correct physical size, + while `X11` and `WL` outputs advertise a 0x0 mm size, leading phoc to + fall back to the default scale value (`1`). + + [ Thomas ] + * Rename call-inbound to call-added. + Rename the call-inbound signal to call-added and remove the check to see + whether a call is inbound and display the call independent from its call + state. This is required for emergency calls to work in #615 and !904. + + [ Sam Hewitt ] + * osd-window: Update to match new design + - changes osd window from fullscreen to top-aligned bubble + * settings: Polish layout when there are not notifications + * lockscreen: Tweak clock animations + * lockscreen: Tweak wwan and battery indicator style. + Close: #272 + * top-panel: Style tweaks. + Improve clock and button styling + + [ InsanePrawn ] + * gitlab-ci: Add networkmanager to alpine deps + + [ Pablo Correa Gómez ] + * monitor-manager: Mark SetOutputCTM call as unimplemented + * monitor-manager: Implement ApplyMonitorsConfigAllowed property. + Otherwise the monitor configuration will be locked and not editable + by the user. + * monitor: Add "n-gamma-entries" property. + The gamma-size handling in wlr is inherintly asynchronous. Therefore, + track the value of "n_gamma_entries" in a property, so consumers + can be notified of changes + * monitor-manager: Implement NightLightSupported property. + Will be needed for GNOME Settings to avoid letting users modify + Night Light when that is not available. + * dbus: Pull latest org.gnome.Mutter.DisplayConfig interface + + [ Nathan Sherwood ] + * build: Use libhandy 1.2 if used as a subproject + + [ Nathan Follens ] + * Update Dutch translation + + [ Pablo Barciela ] + * Update Spanish translation + + [ Balázs Úr ] + * Update Hungarian translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Luna Jernberg ] + * Update Swedish translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Piotr Drąg ] + * Update Polish translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Danial Behzadi ] + * Update Persian translation + + [ Quentin PAGÈS ] + * Add and update Occitan translation + + [ Zurab Kargareteli ] + * Add Georgian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + -- Guido Günther Mon, 23 May 2022 11:11:56 +0200 + +phosh (0.17.0) experimental; urgency=medium + + [ Pablo Barciela ] + * head: Variable 'float' instead 'int' for 'base_scaled_w' + 'floorf' returns 'float' + * Fix [-Wignored-qualifiers] warnings. + * meson: Avoid deprecated 'Dependency.get_pkgconfig_variable' + * Update Spanish translation + * wwan-info: Drop unused macro + + [ Guido Günther ] + * layer-surface: Allow to fetch margins. + This is a bit simpler than using g_object_get() directly. + * layer-surface: Allow to fetch configured width and height. + Avoid g_object_get() roundtrip + * util: Move local_date() from lockscreen. + It's useful in the top-panel too + * arrow: Add doc string + * home: Simplify osk button visibility setting + * tests: Fix typo + * wwan: Make source file name match PhoshWwanManager. + We separate words by '-'. + * vpn-manager: Drop property name prefix. + Use the style from HACKING.md + * settings: Include the abstract WWAN header. + We don't use any backend specific code here. + * docs: Add PhoshWwanManager + * wwan-manager: Add data-enabled property. + This allows to indicate whether mobile data is enabled + * wwan-info: Make source file name match PhoshWWanInfo. + We separate words by '-'. + * wwan-info: Indicate status of mobile data. + We use two arrays with icon names so we can still avoid allocations. + (Closes: #721) + * quick-settings: Follow g-c-c to Settings rename. + We have another mention of org.gnome.ControlCenter in + phosh_get_desktop_app_info_for_app_id() but let's keep that for one more + iteration to ease upgrades. Settings now reports a sane app-id + `org.gnome.Settings`. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/742 + * po: Drop es_ES.po. We have es.po + * build: Document libphosh_{,tool_} sources + * testlib: Clear heads and modes only when used. + * debian: Install portal + * ci: Simplify docker file for i386 builds. + We don't need any packages from experimental anymore + * ci: Use bookworm for i386 build too + + [ Sam Hewitt ] + * data: Add no-data variant of cellular icons + + [ Kai Lüke ] + * Emit WakeUpScreen signal and reset idle timers on resume. + When the device resumes from suspend and there is no user input, + gsd-power remains in the "sleep" state because there is no WakeUpScreen + signal such that even when the new idle timer that gsd-power registered + after resume triggers, the transition from "sleep" to "sleep" is + ignored and the system does not suspend. A second difference to the + behavior of Mutter/GNOME Shell is that on resume there is no reset of + the registered idle timers which should also be fixed to ensure that + Phosh behaves the same. + Act on the logind PrepareForSleep signal (the resume case) to emit the + WakeUpScreen signal and reset the idle timers. + Fixes https://gitlab.gnome.org/World/Phosh/phosh/-/issues/686 + + [ Florian Loers ] + * portal: Add access api. + Implements org.freedesktop.impl.portal + Closes #49 + * data: Install phosh.portal. + This makes xdg-desktop-portal use our access implementation. + + [ free software ] + * po: Update Spanish translation + + [ Martin ] + * Update Slovenian translation + + [ Jiri Grönroos ] + * Update Finnish translation + + [ Мирослав Николић ] + * Update Serbian translation + + [ Prasanta Hembram ] + * Add Santali translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Emin Tufan Çetin ] + * Update Turkish translation + + [ Piotr Drąg ] + * Update Polish translation + * po: Remove .pot file. + Damned Lies automatically generates one for us. + + -- Guido Günther Tue, 22 Mar 2022 15:53:19 +0100 + +phosh (0.16.0) experimental; urgency=medium + + [ Guido Günther ] + * Doc updates + * tools: Add helper to check OSD + * osd: Drop superfluous packaging. Eases the GTK4 port. + * osd: Rely on GTK focus defaults. Smaller UI file and eases GTK4 port. + * gnome-shell-manager: Simplify AcceleratorInfo handling. + It was allocated outside of grab_single_accelerator() but + the details weren't ever needed by the callers so keep things + local so we don't end up leaking again. + * keypad: Drop superfluous receives-default properties + * keypad: Drop superfluous can-focus properties + * doap: Update description + * sm.puri.OSK0: Name method argument. + This generates slightly nicer bindings and documents the + argument in one go. + * Drop osk-stub. + Since this grew more and more features and people accidentally installed + it (#720) move it to a separate repo: + https://gitlab.gnome.org/guidog/phosh-osk-stub/ + * tools: Build app-buttons when tests are enabled. + The screenshot tests use it and abort with a timeout if not + present. + * editorconfig: Bump line length to 100. + This is what we've been using for some time + * background: Don't unref NULL pixbuf. + If no background was ever set we'd generate a critical otherwise. + * phosh-wayland: Make sure to destroy all globals. + Doesn't matter much in practice as they only get created once + * phosh-wayland: Destroy output globals. + Otherwise we leak on output hotplug too. + Since we destroy the output when removing from the hash table + we no longer need to do so explicitly. + * layer-surface: Use g_clear_pointer. Less code and easier to read. + * tests/timestamp-label: Don't leak str + * testlib: Finalize PhocWayland before killing the compositor. + Otherwise we try to cleanup on a dead connection. + * top-panel: Don't leak action_names. + We free when the bindings change but not on dispose. + * monitor-manager: Drop unused variable + * monitor-manager: Use casts. + This will warn us if we get the types wrong + * tests: Generate better backraces with ASAN. + It makes the ASAN runs way slower but backtraces are more useful with + `fast_unwind_on_malloc=0` + Partially fixes 7d5d1c39 ("ci: Drop valgrind run until we have more RAM") + * leak-suppress: Add GDK leaks. + Some things aren't cleaned up by GDK in GTK3. Add these. + * testlib: Drop unused variable + * tests: Add a stub for head handling. + Otherwise we'll produce head related leaks in the tests. + * gitlab-ci: Run phoc based unit tests as well. + This greatly improves the ASAN coverage + * tests-monitor-manager: Remove unused variable + * wifimanager: Don't leak ssid on shutdown + * test-take-screenshot: Free virtual keyboard + * tests: Add osd-window unit test + * fading-label: Add constructor + * Avoid gchar in multiple places + We want to use standard C types where possible + * tests: Add fading label unit test + * build: Move app-auth-prompt to libphosh_tool. This makes it available in + the tests + * tests: Add test for app-auth-prompt + * keypad: Allow to shuffle buttons. Allow them 0-9 to take random positions + while any extra keys remain at fixed positions) + (Closes: #431) + * lockscreen: Allow to toggle shuffle via gsetting. + Enable via + gsettings set sm.puri.phosh.lockscreen shuffle-keypad true + * channel-bar: Don't expose adjustment. + Rather emit the change signal directly. This allows us to e.g. + set the slider to zero on mute while still retaining the old + adjustment value. + * channel-bar: Don't emit volume-changed when muted. + We want to keep the original volume on the stream for unumute + * settings: Set is-muted when channel is muted. + This moves the volume slider to zero but unmute still restores the old + volume. + * channel-bar: Drop zero adjustment. + This should have tracked the volume when muted but never did + * monitor-manager: Fix fd leak. + See https://gitlab.gnome.org/World/Phosh/phoc/-/issues/257 + * docs: Drop wayand protocols. + Since they're not using gtk-doc parsing always gave heaps of errors and + the generated don't have much to say either. + It would be great to have them documented (e.g. by linking to the + upstream docs) but we can do that with the switch to gi-docgen. + * build: Bump meson version to 0.54.0. We'll use the fs module + * protocols: Use custom_target. + This reduces the number of targets from ~3700 to ~2500. + * lockscreen-manager: Fix argument type + * settings: Fix argument type + * call: Use PROP_ID to override id, not PROP_DISPLAY_NAME. + And the other way around. + Fixes: 70dd1f2825d18a23337fb56dad9c469f2858bc29 + * call: Store all param specs. + We'll store one in the follow up commit but let's not have + an array with holes in it. + * call: Handle active-time property. + This makes sure the call time doesn't get reset when locking/unlocking + the shell and call times are in sync with the calls application (except + when the shell restarts). For that we update to libcall-ui 0.0.2 + Closes: https://gitlab.gnome.org/GNOME/calls/-/issues/322 + * Update libcall-ui to latest main. + This picks up some fixes on top of 0.0.2 that eases the build + for downstreams. + * d/gbp.conf: Ignore Marge-Bot's Part-of: + We don't want to clutter the changelog with that + + [ Adrien Plazas ] + * Add PhoshClamp. This will be used to clamp the natural size request of + buttons. + * app-grid-button: Limit the natural width request. + This uses a PhoshClamp to limit the natural width request of the button + in pixels rather than limiting the label's maximum width in characters. + This dissociates the width from the font size, giving us more control + over the button's size, and this allows the button's labels to take more + width if available. + This purposefully leaves the indentation broken to ease review. It will + be fixed in the next commit. + * app-grid-button: Fix the indentation. + It was purposefully left broken in the previous commit. + * Add fribidi dependency. + This will be used to create a fading label. + * Add direction helpers using fribidi. + This will be used to create a fading label. This is copied from + libhandy, which itself copied it from GTK. + * Add PhoshFadingLabel. + This is copied from Libhandy. + * app-grid-button: Use a fading label. + This gives the title a single line as desired by the designers to match + GNOME Shell and iOS, while making more of the title visible and being + less aggressive than an ellipsis. + + [ Sam Hewitt ] + * osd-window: Update the layout and refresh the style. + Reorders the layout of things in the UI and simplify CSS. + * shell: Update lockscreen and notification styles + - puts all the styling for the notifications together + - improved :focus, :active and :hover styles for all buttons + - update notification bubble style + + [ Pablo Barciela ] + * notifications: Reduce the scope of some variables + * phosh-wwan-mm: Reduce the scope of variable 'modem_object_path' + * layersurface: Reduce the scope of variable 'gdk_window' + * util: Reduce the scope of variable 'fd' + * gnome-shell-manager: Fix memory leak. + * gnome-shell-manager: Assert on action id. + Makes things more obvious and the static analyzer happy. + * head: Fix [-Wbad-function-cast] warnings. + * head: Compare float variables with G_APPROX_VALUE + * test-head: Compare float variables with G_APPROX_VALUE + + [ Evangelos Ribeiro Tzaras ] + * gnome-shell-manager: Protect against overflow. + If we overflow we must not allow for any new grabs since + we'd otherwise risk to overwrite old ones. + + [ Danial Behzadi ] + * Update Persian translation + + [ Matheus Barbosa ] + * Update Brazilian Portuguese translation + + [ Fran Dieguez ] + * Add Galician translation + + [ Yosef Or Boczko ] + * Update Hebrew translation + + [ Marc Riera ] + * Update Catalan translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + -- Guido Günther Wed, 23 Feb 2022 16:57:35 +0100 + +phosh (0.15.0) experimental; urgency=medium + + [ Guido Günther ] + * wifimanager: Create NetworkAgent async. + This avoids a `g_main_loop_run` in the sync init. + * monitor: Fix indentation + * doc-check: Allow to specify dir to check + * doc-check: Avoid whitespace that breaks the check + * wl-buffer: Move create_shm_file to utils. + This can be used in multiple places + * monitor-manager: Use phosh_create_shm_file. + This fixes a missing error check for the returned fd as well. + * toplevel-thumbnail: Use phosh_shm_file () + * run-command-manager: Fix section name and log domain. + Drop the additional "dialog-" which is not part or the class name. + * media-player: Only load file URIs. + Data from the network needs to be fetched async since it + can stall. (Closes: #583) + * testlib: Ignore warnings and criticals from the compositor. + (avoids test failures with older phoc) + * gitlab-ci: No need to forcibly remove lcov anymore. + Debian's gcovr doesn't depend on lcov anymore + (https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=987818) + * gitlab-ci: Use newer docker image. + This doesn't forcibly remove lcov anymore which makes e.g. adding + packages easier. + * system-prompt: Handle mnemonic in choice label. + Some of the prompt use it (e.g. gpg prompts for "save in password + manager"). Otherwise we'd just get a leading '_'. + Fixes: de1b395e ("system-prompt: Make sure the choice label wraps") + * system-prompt: Use consistent spacing. We use 6px between label and + button. + * toplevel-thumbnail: Drop flag check + phoc <= 0.10.0 sent y_invert although we didn't use it. Phoc + >= 11.0 doesn't send it triggering the critical so drop it. + * run: Drop gnome session setup. + Most of the time people want phosh+phoc so the session just causes + trouble. We can drop it and people can needing a session can just fire + it up. Adjust the README accordingly. + * run: Always start unlocked. + Running nested is the main use case and one usually wants an + unlocked shell there so run unlocked by default. + If people want to run locked they can pass '-L'. + Adjust docs accordingly + * README: Drop install step. One can run fine out of the source tree and + this avoids spreading files all over /usr/local. + * README: Make phoc config the first argument. + Makes it easier to edit phosh's run options + * README: Use a screenshot of the unlocked shell. + Since this is what we're launching in the example above. + * run: Make it simple to override debugging. + While full debugging is often useful sometimes one wants to limit to + certain modules so honor a set `G_MESSAGES_DEBUG`. + * README: Use ```sh for shell snippets. + Be consistent. Also drop leading whitespace to ease copy/paste. + * README: Fix path to gitlab-ci file + * README: Drop section on libhandy. It can be used as a subproject + * run-command-dialog: Split submitted and cancelled signal. + This will simplify processing when we handle errors better. + * run-command-manager: Fix indentation + * run-command-manager: Return success/failure when running command. + Swap success/failure case while at that to avoid a negated expression. + * run-command-dialog: Drop unneeded can-focus properties + * run-command-{dialog,manager}: Show error message when command failed + * run-command-dialog: Use on_ prefix consistently. We allow for both + (preferring `on_`) but don't want to mix in the same class. + * background: Allow absolute paths too. + When e.g. gnome-photos sets a background it uses an absolute path. + * util: Add helper to markup escape text. + This either escapes all markup or preserves elements allowed + via the notification spec in a notification body. Based on + what gnome-shell does as of commit + 919c4cf3d5657dd93f0c14c21a30071b8e90f6e2. Bugs introduced are + my own. + * notify-manager: Escape notification bodies. + Otherwise bodies with markup would silently be dropped. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/690 + * system-modal-dialog: Mark swipe-away-bin as vertical + * stylesheet: Sort colors alphabetically. + Makes things easier when we add more + * gitla-ci.yml: Don't build tests when building the docs. + This brings down compilation time and resource usage and + the tests don't contribute to the docs atm. + * system-prompt: Don't access priv before checking the type + * tests: Name screenshot suite as such. + The tests aren't manual anyway since they're run by default. + * Move network-auth related bits out of wifi-manager. + Other connections types such as VPN want to do auth too + * network-auth-prompt: Remove unused nmclient + * network-auth-prompt: Drop custom Esc-key handler. + Rather only use the one from system-auth-prompt since that is also + wired up to properly cancel the network-auth request via + `on_dialog_canceled()`. + Otherwise e.g. VPN auth prompts can hang up to timeout + * network-auth-manager: Use cancellable for agent registration too. + We want to cancel this in case of e.g. early shutdown. + * network-auth-manager: Use a separate cancellable for registration. + NM doesn't like if we cancel a successful operation later on rather + crashes in NMSecrentAgentOld's _register_cancelled_cb since the task data + is already freed. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/673 + * newtork-auth-manager: Drop unused nmclient. + Now that network-auth-prompt dropped it we can drop it here too after + fixing cancellation. + * contrib: Add vpn plugin related bits. + Bring over the VPN plugin related bits as from gnome-shell's + shell-network-agent as of 919c4cf3d5657dd93f0c14c21a30071b8e90f6e2. + * network-auth-{manager,prompt}: Support VPN authentication. + Heavily inspired by what NetworkManager's secret handling. Errors + introduced are my own. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/630 + * network-auth-prompt: Use request_id to check if we're done + * network-auth-prompt: Don't allow to reuse prompt. + This allows the the auth-manager to figure out if we're already + busy. + * network-auth-manager: Cancel overlapping requests. + Don't drop them to floor silently anymore. Later on we + should just put them on a queue and process them one + after another. + * wifimanager: Drop unused set_property + * Add vpn-manager to track the vpn status. + Minimal implementation, will be expanded once we add a quick setting. + It will later be used in the quick setting to toggle VPN on/off, etc. + * shell: Spawn vpn-manager + * Add vpn-info to display VPN state. + Minimal implementation, will be expanded once we add a quick setting. + * top-panel: Add VPN indicator. + This displays the VPN state in the top-bar. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/631 + * vpn-manager: Handle wireguard connections as VPN connections + * gitlab-ci: Set defaults. Let's retry jobs on infra changes, make them + interruptible and not run "forever". + * gitlab-ci: Indicate it's a remote include. + * gitlab-ci: Add MR sanity checks. + These are taken from mesa and spot things like unsquashed fixups, etc. + The commit-rules are based on what gnome-shell does. + * README: Update to match reality. + The XMPP channel and mailing list are unused so drop these + together with the broken reference to developer.puri.sm which + lost some redirects when it got moved recently. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/705 + * session: Swap entries + xdg-desktop-portal does a left to right lookup - this makes sense as we + go from specific to generic. So swap the entries. + * build: Make it easy to build osk-stub conditionally. + This avoids people installing it accidentally, messing + up their installs. + * osk-stub: Uncrustify. + Reformat so further changes are simple + * osk-stub: Remove unused variable. + * osk-stup: Use EXIT_FAILURE. + * osk-stub: Init gtk and listen to registry. + This will allow us to hook up the input-metnod protocol + * osk-stub: Add input-method stub. + This allows us to trace input-method requests + * osk-stub: Add an input surface. + * osk-stub: Animate show/hide of the input surface. + Do this when it is actually committed + * osk-stub: Add debug output to intput-surface. + This makes it easy to see ongoing changes + * osk-stub: Add force-show debug flag. + Usually we want to hide the surface when not used. Add a flag to + always show it so we can trace activate/deactivate better. + * build: Allow to disable the tools build. + They are rarely needed but add on the build time so don't + build them by default but in CI. + * osk-stub: Indicate GNOMEs screen-keyboard-enabled setting. + * app-grid-button: Drop unused variable. + * activity: Allow to activate by keyboard. + Activate the selected activity when hitting return + (Closes: #701) + * settings: Hide docked quick setting when not docked. + It's only useful when we're there's enough hardware for docked + mode. + * settings: Hide torch quick setting when no hardware is found. + We need to add the flowbox child for that since just hiding the + QuickSetting won't make the FlowBox give up on the FlowBox Child's + space. + * data: Move icons into subdirectory. + This unclutters data/ + * data: Add icon for disabled vpn. + Part-of: + * vpn-manager: Track present and last connection. + We indicate VPN presence when at least one VPN is configured. + * settings: Add VPN quick setting. + We only show it if there's at least on VPN configured. Toggling + the quick setting either shuts down an active VPN or activates + the last active one. + * osd-window: Disable keyboard interactivity. + We don't want keyboard input in the OSD anyway but this + additionally makes sure the focused client doesn't reenable + text-input potentially unfolding a manually hidden keyboard. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/706 + Part-of: + * settings: Print panel name on error. + We free it in the callback now than after invoking the + DBus call. + Part-of: + * quick-setting: Use const char * for panel. + This indicates that we don't transfer any ownership + Part-of: + * quick-setting: Check return value. + When the return value is `NULL` an error occurred. + This also tricks the compiler in not treating the variable + as unused. + Part-of: + * gnome-shell-manager: Drop NULL check + g_new0() terminates the application if allocation fails + Part-of: + * monitor-manager: Drop unused variable. + Fixes a6bab85e ("Allow to switch output configurations") + Part-of: + * status-icon: Don't leak icon-name. + When getting the property we phosh_status_icon_get_icon_name() + which is transfer-full. + Part-of: + * run-command-manager: Avoid unnecessary copy. + There's no need to build a GPtrArray and turn that into a GStrv + when we can use the GStrv directly (which also leaked). + Part-of: + * top-panel: Avoid unnecessary copy. + There's no need to build a GPtrArray and turn that into a GStrv + when we can use the GStrv directly. + Part-of: + * tests/mount-notification: Don't leak name. + Part-of: + * tests/util: Don't leak. + Make sure we free the allocated markup and add macro to make things + more concise while at that. + Part-of: + * tests/status-icon: Don't leak icon name. + It's returned as transfer-full. + Part-of: + * tests/notification: Fix leaks. + The values returned from `g_object_get()` are always allocated. + Part-of: + * tests/notification-source: Fix leak. + The values returned from `g_object_get()` are always allocated. + Part-of: + * meson: Pass ASAN leak suppression automatically. + No need handle these manually. + Part-of: + * gitlab-ci: Run unit tests under ASAN too. + We currently only run the "pure" unit tests since the other + ones need more cleanup. + Partially fixes 7d5d1c39 ("ci: Drop valgrind run until we have more RAM") + Part-of: + + [ Sebastian Krzyszkowiak ] + * Switch to wlr-gamma-control protocol (Closes: #62) + * app-grid: Debounce app search. + * settings: Set/unset mute when setting output stream volume. + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Pablo Barciela ] + * Fix [-Wmissing-field-initializers] warnings inside GActionEntry. + * shell-network-agent: Fix [-Wmissing-field-initializers] warnings. + * GActionEntry: use designated initializers. + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Evangelos Ribeiro Tzaras ] + * gitlab-ci: docker: Fix conditional. + + [ Fabio Tomat ] + * Update Friulian translation + + [ PanzerSajt ] + * lockscreen: Allow to show OSK to enter passwords. + Add a button to the keypad that unfolds the osk and shows + a text Entry. This allows for arbitrary passwords + + [ Anders Jonsson ] + * Update Swedish translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Alexander Mikhaylenko ] + * swipe-away-bin: Add hide() and reveal() + Support programmatic revealing and not just removing. + * swipe-away-bin: Make orientable + * swipe-away-bin: Add property to allow swipes in the other direction + * swipe-away-bin: Add reserve-size property + * panel: Use GtkGestureMultiPress and released signal for closing. + Make sure we don't interfere with swipes in future. + * notification-frame: Reimplement activation via GtkGestureMultiPress. + Unfortunately, in GTK 3 it's not possible to use single click row + activation together with swipes; they will interfere. Meanwhile, we still + need activation for keynav, so switch to double click activation and + implement it manually instead. Use this chance to unify click handling with + the header. + * notification-frame: Support swipe-to-remove + + [ Sam Hewitt ] + * settings: Use speakers icon for volume slider + * settings: Move media player above notifications box + * settings: Add name property to clear all button + * settings: Add style class to notifications header box + * media-player: add style class to art + * lockscreen: Add style classes to icons and text + * lockscreen: Use full width for notifications and media player + * stylesheets: GTK overrides for sliders. + Make them look more like in the current mockups. + * settings: Adjust quicksettings radii + * lockscreen: Give the date and time more weight + * stylesheet: Adjust modal dialogs to mockups. + Most notably add a margin. + * overview: remove background and borders from shell scrollbars + hides unneeded gtk stylings from the scrollbar troughs mainly in the + overview. + * overview: Add focus style to the search box + - remove some gtk styling + - mirror the focus styling of the search box on desktop shell + * overview: Improve grid separator styling + - Add margin to the separator for better visuals + - Use named colour to draw it so it isn't lost on light theme + + [ Danial Behzadi ] + * Update Persian translation + + [ Vittorio Monti ] + * Update Italian translation + + [ Matej Urbančič ] + * Update Slovenian translation + + -- Guido Günther Sun, 23 Jan 2022 13:37:35 +0100 + +phosh (0.14.1) experimental; urgency=medium + + [ Guido Günther ] + * Fixup margin-{start,end} vs margin-{left,right}. The later ones are + deprecated and complicate the GTK4 migration. + * osk-manager: Drop `PHOSH_OSK_` prefix from property enum. + Make it match what we use in HACKING.md + * osk-manager: Use shell to listen for lock status. + * osk-button: Make sure we disconnect signals when button goes away. + When the button gets destroyed we need to disconnect the signals + from osk-manager. + * ui: Drop all glade markers. + These are spread around although files aren't glade edited. + * wifimanager: Don't drop unimplemented prompts silently. + Instead of just doing nothing cancel the request so NM doesn't + end up waiting until we hit a timeout. This mostly affects + VPN prompts. This makes sure we don't interfere with e.g. + nmcli c up + Related https://gitlab.gnome.org/World/Phosh/phosh/-/issues/630 + * app-grid: Ellipsize app filter button. + This prevents horizontal scrolling when users select large fonts. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/645 + * lockscreen: Remove out of place '>' + Fixes: 316a65c7 ("lockscreen: Allow to accept incoming calls") + * Bring over HdyKeypad from libhandy 1.2.2. + Adjust by dropping unused features. We do this in one commit to keep + bisection intact. We use phosh-keypad as CSS class to not conflict with + libhandy's keypad css class. + * keypad: Derive from GtkGrid. + GtkBin does not exist in Gtk4 so derive from GtkGrid directly. + It's o.k. to expose the grid/container API here since this isn't + a shared lib. This simplifies the GTK4 migration. + While at that drop make it a non-derivable type for now. + * dbus: Update calls DBus interface. + Taken from calls commit 8f9f7311fa7095492c2c900a23610e3b86a874dd + * call: Add avatar-icon property + * tests: Use avatar in screenshots. + We use cat.jpg from gnome-control-center + License is GPL-2+ which is compatible with the demo app. + * tests: Screenshot uk locale too. + It's usually very complete and uses we get a + good idea about cyrillic language layout. + * subprojects: Forward to latest libcall-ui. + This includes the textdomain fixes. + * tests: Fix translations for call related screen shots + * debian: Don't install libcall-ui translations. + They're shipped by calls. + * d/copyright: Flip sections. + This makes lintian happy. + * notify-manager: Set urgency. + Don't forget to set the urgency when parsing the hints. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/653 + * notify-manager: Don't use a fallback icon for critical notifications. + See Tobias comment in + https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/938 + * docked-manager: Ensure mode_changed_cb() sets all values on startup. + If can_dock is False the mode_changed_cb() triggered from + docked_manager_constructed() exits early letting the GSettings at their + previous values. Make sure we always set these initially since we mode + before e.g. device shutdown might be different from the mode on boot. + * build: Drop glib fallback. + Since we don't require more recent glib than 2.62 we can also drop the + fallback. + Fixes: 8f37b6c9 ("tests: Drop g_test option") + * layersurface: Make debug message more useful + * shell: Add and use phosh_shell_set_builtin_monitor() + More descriptive than `on_monitor_added()` and the later will + do more soon. + * shell: Simplify by using g_set_object. + Move clearing and setting to one step that will also handle NULL + correctly. + * Allow the primary monitor to be NULL. + This can happen when all monitors are gone. + We basically need to make sure we don't create surfaces on gone devices + but add them once a new monitor shows up. + * shell: Remove primary-monitor startup workaround. + Since we handle NULL now we can just catch up once a primary monitor is + there. + * shell: Trigger selection of a fallback monitor when all went away. + When all monitors went away we need to select a new one to enable. + * shell: Move find_builtin_monitor upwards. + We'll use it from phosh_set_builtin_monitor soon. + * shell: Rename phosh_set_builtin_monitor + use phosh_shell_ prefix as with other methods + * shell: Update builtin monitor in on_monitor_removed as well. + * shell: Make sure we pick a new builtin monitor. + Since monitors get removed at the very end of their live from the list + of monitors we need to make sure we don't pick the same one when looking + them up. + * shell: Move compositor state notification to its own method + * shell: Use compositor notifications on primary flips. + When there's no primary monitor left let the compositor know we're in a + recovering state and notify it when things went back to normal. This + allows it to fade us in properly. + * monitor: Split finalize and dispose + * monitor-manager: Release wayland resources sooner. + Start disposal of the monitor early so wayland resources are already + released and hence can be re-added when e.g. a display of the same name + shows up again. This mostly affects wlr-output-power. + * monitor-manager: Clear pending head states on errors. + Makes sure there are no pending head state changes when aborting + a config, this might otherwise leak to other head changes in the + future e.g. via phosh_monitor_manager_set_monitor_transform(). + * monitor-manager: Make it clear that the we were invoked with invalid data. + The issue is not that we didn't get a primary monitor but rather that + the sender didn't specify one. + * monitor-manager: Fix protocol name in debug message + * monitor-manager: Allow to select primary that is currently disabled. + * ci: Always print error logs on failure. + Makes debugging easier + * testlib: Print compositor startup message. + This helps to figure out when it started + * testlib: Terminate comopsitor on SIGTRAP. + Currently meson tests time out since the test/shell process ends on e.g. + a SIGTRAP caused by a g_critical () while the compositor process keeps + running making meson wait until it hits the timeout. + * po: Remove panel.c from POTFILES.in too. + Fixes: 08db8d89 ("Rename panel top top-panel") + * po: Fix POTFILES.skip. + The syntax didn't make `intltool -m` happy causing the + check-po test to fail silently. + Fixes: 0343af09 ("po: Use wildcard to ignore subprojects") + * run-command: Use 'activate' signal. + Avoids GDK event filtering. + * system-prompt: Make sure the choice label wraps. + Use a separate label where we can set the "wrap" property. + + [ Pablo Barciela ] + * gnome-shell-manager: Fix show error, inside 'on_bus_acquired' function + * app-tracker: Fix dereference of a null pointer. + * notification-content: avoid duplicate break + + [ Hunman ] + * app-grid: Sort with collation + + [ Dorota Czaplejewicz ] + * gitlab: Improve bug report template + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Luna Jernberg ] + * Update Swedish translation + + [ Florian Loers ] + * Add run-command dialog with Alt+F2 shortcut. + Closes #610 + + [ Anders Jonsson ] + * Update Swedish translation + + [ Sebastian Krzyszkowiak ] + * activity: Fix cut out thumbnails. + Since 117e397da924093573e8dec57649b8615a7b079e, thumbnails were + often cut out at the bottom. This was because get_scale function + was checking size of a different widget than before refactoring. + * ui: Move Activity's 6px bottom margin from GtkButton to Overview's + Carousel. + For thumbnail sizing to work correctly, PhoshActivity shouldn't have other + margins than the ones defined by GtkDrawingArea. + * overview: Use phosh_shell_get_usable_area for activity aspect ratio. + Previously we effectively used the aspect ratio of PhoshOverview itself, + which wasn't exactly the same as the ratio of most maximized applications. + * activity: Take margin into account in get_preferred_width_for_height. + The aspect ratio we're interested in is the one of GtkDrawingArea, so to + get the aspect ratio of whole PhocActivity need to compensate for drawing + area's margins. + * activity: Add phosh_activity_get_thumbnail_allocation + * overview: Request thumbnails based on GtkDrawingArea's size. + Previously we used the size of whole PhoshActivity there, which isn't the + same as we now have a bottom margin on GtkDrawingArea. + * activity: Use GtkDrawingArea's context to determine its background. + Using PhoshActivity doesn't make much sense there since it can + already draw its own background. + * activity: Use CSS to apply background for maximized windows + * activity: Rename CSS classes, use widget name where possible + + [ Evangelos Ribeiro Tzaras ] + * subprojects: Forward to latest libcall-ui. + This includes the UI tweaks and fixes. + * call: Reindent + * dbus: Update calls DBus interface. + From calls revision a28d6946238e46d37d1a750fb9bde70d3254d74b + * call: Hook up CanDtmf property. + This controls whether CuiDisplay allows revealing the dialpad. + * call: Hook up SendDtmf DBus method. + This sends the dtmf tone to Calls. + * css: lockscreen: DTMF keypad should respect phoshs background color. + The keypad is a child of a GtkActionBar in CuiCallDisplay which has the + background style class set. Setting the background color for the GtkActionBar + in CSS ensures that phoshs styling is respected. + + -- Guido Günther Wed, 01 Dec 2021 18:22:30 +0100 + +phosh (0.14.0) experimental; urgency=medium + + [ Mohammed Sadiq ] + * wifimanager: Use hotspot icon if wifi hotspot is active + + [ Guido Günther ] + * notify-manager: Move schema paths and key defines to header. + Fixes the leftover from a4a338b70c12a6d0187d7bf9ae9227abf46b767e + * osd: Hide label when empty. + This avoids having the OSD out of center when the label is empty. + * phosh.service: Reduce boot flicker. + Take over tty1. We currently don't take over quitting plymouth + but we'll do so once we wire in a display manager. + * torch-manager: Don't emit a warning when no torch is found. + This makes running with G_DEBUG=fatal-warnings harder than necessary. + * system-modal-dialog: Drop `can-focus` properties that use default value + * util: Add helper to strip '.desktop' from app_id. + Needed in several places + * Add PhoshSplash. + Splash screen for launching applications. It times out after + 5 seconds if not closed. + * Add marshalers list. + We want special marshalers for more complex signal arguments. + * phosh-private: Update phosh-private protocol from phoc. + This gives us some more information when apps launch. + * phosh-private: Emit signals when we receive startup-ids. + This will allow other parts of the shell to track application + start. + * phosh-wayland: Allow to retrieve phosh_private protocol version + * wayland: Use defines instead of hard coding protocol versions. + We have multiple places where we check versions of phosh_private, + use proper defines for readability + * Add app-tracker. + This keeps track of spawned apps (via glib's g_app_info) and + corresponding startup_id's (mostly via gtk_shell1 protocol). + Additionally we track org.gtk.gio.DesktopAppInfo on DBus + This allows us to show splash screens for apps that are spawned by other + processes than the shell itself. E.g. + gtk4-launch gnome-control-center.desktop + or when opening a file in nautilus. We only do something if we get a + desktop file name and a startup id (the later is e.g. not the case when + processes are spawned via 'gio launch' or gnome-contacts). + * app-grid-button: Use app-tracker for app launching. + A button shouldn't care about the details of app launching. Let + the app-tracker handle that. + * Add splash-manager. + The SplashManager listens to app-tracker events and shows/hides + splash screens as needed. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/183 + * shell: Add and parse debug flags. + This makes it simpler to test certain aspects of the shell. + * splash-manager: Only show splash when not docked + * splash: Allow to prefer dark theme. + Allow to set the dark/light theme preference during construction. + * splash-manager: Honor global dark theme preference. + Although apps can still opt out this way the splash is way more likely + going to match the launched apps color scheme. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/618 + * monitor: Treat wlroots virtual connector as such` + The wayland, x11 and headless backends don't correspond to real + hardware so treat their connectors as virtual. + * mode-manager: Unclutter external monitor check. + This makes future additions simpler + * mode-manager: Handle virtual monitor. + If a system has a single output and that is virtual we treat it as not + having external display hardware. This allows to test the undocked + case more easily with virtual wayland backends like headless, x11 or + wayland. + If we want to test docked mode we can add a second output. + * docked-manager: Treat unknown hardware as undocked. + If modem-manager doesn't have enough information to set a hardware + type (no logind, etc) assume undocked rather than docked since + we want to default to phone mode. + * tools: Fix paths to CSS. + * d/control: Add more recommends. + Add proper fonts and the SVG renderer. This is useful on minimal + installs like in containers. + * tests: Use the correct flags when creating DBus proxy. + There's nothing object manager related there and we create a + GDBusProxy in the end. + * build: Add a suite to all tests. + Ignore manual tests on the alpine build + * gitlab-ci: Collect junit test reports + gitlab can handle those for better test failure output. + * build: Bump required meson to 0.52.0. + This avoids a warning and 0.56.0 is in Debian stable. + * media-player: Use cancellable. + Don't hold a ref across async calls but rather check if the + operation was cancelled. This makes sure we can properly + dispose and don't end up in half finished async calls. + * tests: Drop g_test option. + Rather check if we're using recent enough glib at runtime + * screenshot-manager: Assert on g_file_test. + Just calling it isn't enough. + * gitlab-ci: Use meson to run the tests. + Ignore manual tests on alpine + * README: Update test instructions. + The manual tests need extra setup so skip those, we also don't + need extra gtest invocation since some time. + * tests: Use minimal device resolution for phoc. + This makes screenshots, etc more useful + * tests: full-shell tests: Use temporary file for XDG_RUNTIME_DIR. + This allows to run tests in parallel. They were previously racing for the + wayland socket. + * tests: Run more tests in parallel. + * gitlab-ci: Switch Debian builds to bookworm. + We keep the cross build at bullseye for the moment to + stay out of dependency problems. + * gitlab-ci: Generate locales needed for the screenshot tests + * gitlab-ci: Fix registry URL. + We want the one from the blessed repo not a user's fork. + * testlib: Init libcallui as well. + We should init all libs used phosh (that for the "normal" shell get + inited in phosh's main) + * testlib: Allow to send modifiers via virtual keyboard as well. + We use keycodes for those too so users only have to worry about + KEY_* and not a mixture of wl_keyboard modifiers and keycodes. + * top-panel: Handle toggle-message-tray keybinding. + This brings up the settings menu (which contains the message tray) + * tests: Take screenshots. + Take screenshots for different locales during CI. This allows for quicker + validation of how things look in different locales. + * gitlab-ci: Expose test results and screenshots prominently. + Add a separate job that takes the screenshots and use a container that has + some more dependencies so we get the fonts right and images rendered + correctly. + * batteryinfo: Show missing battery icon when we can't connect to upower. + This makes the quick settings look less odd in e.g. tests. + * batteryinfo: Make widget insensitive without battery. + No point in having it look available. + * wifimanager: Always return an icon name. + We have the `present` property to figure out if wifi is actually + there so always return a sensible icon name to make it simpler + for consumers to show something useful. + * gitlab-ci: Condense all screenshots of a locale into a single image. + This makes it simpler to get a quick overview. + * calls-manager: Drop out early on duplicate call ids. + Should we ever see this we better not reconnect signals etc + but rater ignore the duplicate id. + * testlib: Add a simple call mock. + This can be used whenever we need an incoming call + * app-tracker: Adjust to "launch-started" DBus API. + GLib 2.72 emits "launch-started" for spawned and DBus activated apps. + Track this too and use that instead of "launched" to bring up the splash + since the later is emitted only once the app is up which is too late for a + splash. + There's some backward compatibility code to try get reasonable results + with current released glib but for DBus activated apps you really need + 2.72.0. + * app-trcker: Handle timeouts. + So far we'd not properly dispose AppState if the app never + showed up nor failed. + With this we can drop the timeout handling from the splash. + * splash: Remove timeout. + This is now handled by the app-tracker. + * notifications: Reduce spacing between content and header. + Fixes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/286 + * monitor: Take mode into account for fractional scale. + Thanks Pablo Correa Gomez for the analysis + * splash-manager: Don't leak schema + * settings: Use headphone icons when headphone is plugged + * settings: Stop media player playback when headphone gets unplugged. + We do this form settings since this is always around. + * media-player: Use rounded buttons + * Move adaptive apps filter to the bottom. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/628 + * splash: Use same icon shadow in light and dark mode + * wifimanager: Simplify switch. + * wifimanager: Use g_set_object to set dev. + Just so we use the same throughout this file + * wifimanager: Unref connection on update. + When there's a new connection make sure we unref the old one and + disconnect signals. So far we would leak the ref here. + * wifimanager: Unref connection when there's no active one. + So far we would leak the ref here. We can also drop the dev->dev check + since `cleanup_device ()` checks that too and if we cleanup the device we + want to clean up the AP too since that is bound to the device. + * media-player: Reset artist/title/url without metadata. + If we can't get any metadata reset to the defaults. So far + we'd only reset if we got partial data at least. + * media-player: Don't clear title and artist when player stops. + Some players like Shortware don't send new metadata when resuming + playing after stop if it didn't change. This makes sure we have it still + around. + (We also failed to reset the icon) + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/375 + * call: Drop unused include + * shell: Fix callback signature. + We don't use that param but it's better to be correct here. + * protocol: Update to latest phosh-private + * shell: Notify compositor that shell is ready. + We still have some blocking calls and fetching all the apps isn't async + either yet. So delay for a second until we signal to the outer world. + * d/control: Depend on phoc > 0.9.0. + This one supports '-S' to wait for the shell. + * session: Enable shell mode. + Make phoc wait until phosh attaches before unblanking + the screen. + * media-player: Make play button circular + * media-player: Don't use `-1` as icon size. + This triggers warnings. Since we want to avoid passing + GTK_ICON_SIZE invalid or encode in two places set the + property directly. Upside is that this also works with + GTK4. + * media-player: Make play/pause icon a bit smaller. + Use 24px instead of 32px. The other icons are 16px. + Closes: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/632 + * activity: Use the correct background style for drawing the background. + The grey border looks odd otherwise which is even more pronounced in + high contrast mode. + Helps: https://gitlab.gnome.org/World/Phosh/phosh/-/issues/629 + * activity: Move the close button over the thumbnail. + Before the mouse hovered close button be outside the thumbnail since + windows aren't all the same width in e.g. docked mode. This calculates + the margin from the allocated width and the thumbnail's size. + * activity: Always align thumbnails to the top. + This doesn't affect maximized windows but makes sure the close + button is on top of the window. + + [ Vittorio Monti ] + * po: Update Italian translation + + [ Evangelos Ribeiro Tzaras ] + * proximity: Only act on changes if we claimed the sensor. + Fixes #624 + + [ Oliver Smith ] + * editorconfig: treat *.ui like xml + * media-player: add seek buttons. + + [ Florian Loers ] + * activity: Drop activity labels. + Remove the label in the activity overlay and move the icon into a centered + position. Fixes: #330 + * activity: Remove title field. + * activity: Add icon-dropshadow to activity icon. + + [ Sebastian Krzyszkowiak ] + * util: Introduce phosh_get_desktop_app_info_for_app_id. + This replaces phosh_fix_app_id and allows for logic in PhoshActivity + and PhoshAppTracker to become a bit simpler. + We get rid of "org.gnome." prefix handling (with one exception of + g-c-c which apparently keeps non-revDNS desktop filename for + backwards compatibility) as phoc will now forward application ID + obtained via gtk-shell, which is used by GTK 3 applications. + However, we now also check the last component alone and a lowercase + version in order to increase compatibility with legacy X11 and + non-GTK applications. Examples of apps that now get correctly + matched thanks to that include Mumble, Audacity, Blender, Jamulus + and Darktable. + * phosh_get_desktop_app_info_for_app_id: Add gnome-usage to mappings + + [ Danial Behzadi ] + * Update Persian translation + + [ Nathan Follens ] + * Update Dutch translation + + [ Zander Brown ] + * Update British English translation + + [ Goran Vidović ] + * Add Croatian translation + + [ Hugo Carvalho ] + * Update Portuguese translation + + [ Matej Urbančič ] + * Add Slovenian translation + + [ Jiri Groenroos ] + * Update Finnish translation + + -- Guido Günther Tue, 19 Oct 2021 14:31:17 +0200 + +phosh (0.13.1) experimental; urgency=medium + + [ Guido Günther ] + * d/gbp.conf: Switch development branch to main + * d/control: Bump breaks on gnome-calls. + Not strictly necessary but makes missing deps easier to detect + * ci: Add font to alpine image. + This fixes the overview test crash since pango needs at least + on font in some versions. Thanks to InsanePrawn for the backtrace. + * notification-frame: Align timestamp to the end. + The current center leaves too much space to the right. + * notify-manager: Add method to close all notifications + * settings-menu: Add heading to notifications list-box + * Add callback to clear all notifications (Closes: #570) + * build: libcall-ui moved to /World/Phosh too. + Adjust the submodule URL + * ci: Be verbose when installing alpine deps. + This makes identifying breackage due to changed versions easier. + * media-player: Fix button order + * media-player: Move navigation buttons into a box. + This makes {first,last}-child styling way simpler + * media-player: Drop receives-default properties + * media-player: Remove margin. + This makes it the same width as notifications instead of slightly + smaller which looked very odd. + * media-player: Use the same border radius than notifications. + Looks odd otherwise + * media-player: Switch artist and title. + Left align and dim artist to get closer to designs + https://gitlab.gnome.org/World/Phosh/phosh/-/issues/543 + * media-player: Drop lockscreen styling. + Style it the same way we style in settings menu and the notifications. + Closer to designs in https://gitlab.gnome.org/World/Phosh/phosh/-/issues/543 + * media-player: Improve button layour and styling. + Use larger play/pause button, move ff and rev a bit closer + to the center. + Closer to designs in https://gitlab.gnome.org/World/Phosh/phosh/-/issues/543 + * media-player: Use larger icon. + Center text artist / song vertically to it. This looks odd + with Firefox since it tries to be clever and use 16:9 which + the gicon aligns renders to the top instead of centered. + * lockscreen: Use less margin between elements. + Lower the margin between icons and media player and media player and + notifications quiet a bit to safe space and look more consistent. + * Add initial PhoshMountOperation. + This implements GMountOperation's ask-password to handle th UI bits for + e.g. encrypted volumes. We can add other methods like `ask_question` + later on. + * mount-manager: Use PhoshMountOperation. + This allows allow for interactive prompts to ask for passwords + of encrypted volumes. + With this encrypted volumes can be mounted without going through + nautilus first. (Closes: #573) + * shell: Use fractional scale for phosh_shell_get_usable_area too. + Otherwise fractional scaling is off on the primary display. + Fixes: 540c85c83e6634a7cf90a4f4276339003f10a352 + * overview: Use fractional monitor scale. + This will allow us to drop the integer scale completely. + * monitor: Drop integer scale + * tests: Add test for head scale calculations + * monitor-manager: Support fractional scale DisplayConfig. + This allows to set fractional scale in Settings too. The actual + calculation is based on what mutter does. (Closes: #479) + * Revert "monitor-manager: Flip transform" + This reverts commit 75cad1aaeb76f75b1cd6066392f956e083f6f5ec. + There's no need to flip the transform with recent wlroots/phoc, we + end up flipping the upside down in landscape mode otherwise when + e.g. changing scale. + * Rename panel top top-panel. + This makes it match the ui file + * top-panel: Drop priv. + No need for that when we're never going to derive. + Related: #378 + * home: Don't pass on to search bar + otherwise we end up activating from the search bar twice thus + launching the apps two times (which most of the times doesn't + matter due to the apps single instance pattern but e.g. Firefox + shows the issue). + Fixes 60f6a2e157e60a38ad8b8913882ff1e6fea548a3 + + [ Mohammed Sadiq ] + * d/rules: Disable building tests if 'nocheck' set. + Not building tests reduces the binaries to build by more than half + + [ Pablo Correa Gómez ] + * feedback-manager: Rotate profiles on touch to quick settings. + Currently, there exist three different feedback profiles: + "full", "quiet", "silent", but quick settings touch only + switches in-between "full" and "silent". Instead, rotate + from "full" to "quiet" to "silent" and back to "full" + + [ Marc Riera ] + * Update Catalan translation + + [ Kristjan SCHMIDT ] + * Update Esperanto translation + + [ Daniel Șerbănescu ] + * Update Romanian translation + + [ Michael Oppliger ] + * Update German translation + + -- Guido Günther Sat, 28 Aug 2021 08:19:53 +0200 + +phosh (0.13.0) experimental; urgency=medium + + [ Arnaud Ferraris ] + * torch-manager: use logind instead of upower for toggling the torch. + The previous version relies on a downstream upower extension which was + rejected by upstream[1]. + In order to avoid having to carry downstream packages, this patch + provides a new implementation, based on udev for device discovery, and + logind for enabling/disabling the torch. + [1]: https://gitlab.freedesktop.org/upower/upower/-/merge_requests/50 + * dbus: remove upower torch protocol. + This is no longer needed with the new torch manager implementation + * d/control: add build dependency on libgudev. + This is required by the new torch manager implementation. + * gitlab-ci: add missing dependency for alpine-based jobs + + [ Guido Günther ] + * doap: Add gnome userids + * shell: Select a different style sheet for HighContrast theme. + The stylesheet is the same as the default one except that + it has foreground and background colors swapped. This + allows us to use a high contrast light theme. + Closes: https://source.puri.sm/Librem5/phosh/-/issues/523 + * css: Drop .phosh-settings-row and .phosh-settings-listboxrow. + Code using that was removed in + 5e214df154f733088283d2de6406c2d18dc90548 + * css: Drop duplicated properties + * css: HighContrast: Use a 1px border for panel buttons. + This makes the shell chrome easy to identify. + * home: Make keyboard handling a bit more standard. + Use GDK_EVENT_{PROPAGATE,STOP} and gdk_event_get_keyval and a template + callback. + * overview: Focus search when user starts typing (Closes: #564) + * gitlab-ci: Drop unused vars + * gitlab-ci.yml: Drop and adjust tags. + Adjust tags to GNOME's CI + * gitlab-ci: Publish docs + * gitlab-ci: Speed up package build. + Drop the docs now that we have gitlab pages + * Change URLs to gitlab.gnome.org. + We also adjust the URLs of projects that are bound to move soon like phoc. + * README: mention API docs + * Add dockerfile to create image. + The image has all the build-deps to speed up build and tests. Based + on what libadwaita does. + Use + .gitlab-ci/run-docker.sh --base=debian-cross build--version 0.0. + .gitlab-ci/run-docker.sh --base=debian-cross push --version 0.0. + to update. + * gitlab-ci: Use custom docker image for most jobs. + This also does away with the global `before_script:` + * gitlab-ci: Use prebuilt image for 32bit build too + * dbus: Update calls protocol. + This adds the needed bit to accept calls on the lock screen. + * Add libcall-ui as submodule. + This allows us to share the call ui between calls and + phosh without coupling them further. + To build we also need libcallaudio-dev since that's a + dependency of libcall-ui. + * main: Initialize libcall-ui + * phosh-call: Add DBus based implementation of the call-ui-call interface. + This wraps the DBus interface in a PhoshCall's object that implements + CuiCall so it can be used with CuiCallDisplay. + * lockscreen: Allow to accept incoming calls. + Use CuiCallDisplay to handle incoming calls. + Relevant mockups: https://gitlab.gnome.org/Teams/Design/os-mockups/-/blob/master/mobile-shell/lockscreen-calls.png + * call: Handle can-dtmf. + This makes sure we keep the dialpad disabled until the + DBus interface handles it. libcall-ui has all the bits + already and adding DBus support will be backward compatible. + * schema: Calls moved to org.gnome. + Adjust the favorite. + * Move the calls DBus interface to org.gnome.Calls. + This changed with the move to upstream GNOME. + * monitor: Allow to get fractional scale. + This is derived from the logical size as described in + https://wayland.app/protocols/xdg-output-unstable-v1#zxdg_output_v1:event:logical_size + * background: Scale by fractional output scale. + Scale the background by logical scale instead of what we get + from xdg_output->scale. This allows the background to scale + with fractional scaling. (Closes: #338) + * tests: Stub phosh_shell_get_locked() + This will be used by PhoshNotifyFeedback and is hence needed + in the standalone notification (test) server. + * Move feedback for notifications to a separate class. + It was o.k. to have it in settings since that was the only long term + notification store and we just used a single feedback event with + - notifications also being on the lock screen + - sending different event feedback + we don't want to add more complexity to that single notification + display. This also stops abusing the ListBox's create_row callback to + handle feedback events. + * notify-feedback: Adjust event name based on notification category. + Adjust the emitted feedback based on the notification category¹. We could + use the sound-name instead but that has the problem that the application + sometimes has a hard time to figure out what the appropriate event is. + E.g. `message-missed-instant` and `message-new-instant` are hard to tell + apart from an application PoV. + 1: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html + * panel: Connect to "setting-done" only once. + No need to connect on every unfold. + * tests: Add stubs for notify-server-standalone. + This keeps things compiling + * notification-content: Allow to show/hide body and actions + * notification-frame: Add show-body property. + This allows to show/hide the bodies of contained notifications + * lockscreen-manager: Allow to scroll to a lock screen page programmatically. + This will be used to scroll to the unlock page when the user activates a + notification. + * lockscreen: Display notification summary + * lockscreen: Add missing guard to public function + * notify-manager: Trigger scroll to unlock page if shell is locked. + If the user activates a notification and the shell is locked scroll + to the unlock page. + * lockscreen: Handle "show-in-lock-screen" gsetting + * gresources: Sort icons alphabetically + * feedback-manager: Use separate icon for quiet mode. + This makes the current mode easier to identify + + [ Mohammed Sadiq ] + * panel: Hide power popover on tapping topbar if open. + If the user taps on the notification bar/panel, + hide power popover (if open) instead of hiding the panel. + * panel: Return GDK_EVENT_PROPAGATE in button-press-event handler. + GDK_EVENT_PROPAGATE documents the purpose better than FALSE + + [ Pablo Correa Gómez ] + * feedback-manager: Rotate profiles on touch to quick settings. + Currently, there exist three different feedback profiles: + "full", "quiet", "silent", but quick settings touch only + switches in-between "full" and "silent". Instead, rotate + from "full" to "quiet" to "silent" and back to "full" + + [ Zander Brown ] + * notifications: Dim icon. + See https://source.puri.sm/Librem5/phosh/issues/286 + * notifications: Use a bit less space between icon and title. + See https://source.puri.sm/Librem5/phosh/issues/286 + + [ Anders Jonsson ] + * Update Swedish translation + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Мирослав Николић ] + * Update Serbian translation + + [ Vittorio Monti ] + * po: Update Italian translation + + -- Guido Günther Mon, 09 Aug 2021 11:06:16 +0200 + +phosh (0.12.1) byzantium; urgency=medium + + [ Guido Günther ] + * d/control: Bump phoc recommends. + Recommend a version that allows us to bind all the interesting + keys. + * rotation-manager: Clarify debug message. + Otherwise it can be confused with the rotation mode + * rotation-manager: Don't claim accelerometer with orientation-lock. + This reduces the work for iio-sensor-proxy a lot improving battery life. + * shell: Move public functions to the end of the file. + This makes it consistent with HACKING.md + * shell: Add guards to public functions + * shell: Protect against lockscreen-manager not being up yet. + We get the monitors very early and the lockscreen_manager is not yet + created. Avoid a critical (and potential crash if we wiggle code around + later on). + * overview: Only lookup activity if needed. + No need to lookup the activity if the toplevel didn't get activated. + * overview: Change activity type. + This avoids some casting + * overview: Unset activity when toplevel goes away. + This prevents us from trying to set a focus on an + inexistent activity in phosh_overview_reset. + Fixes: 009177fac12b41d780ffc255bbb1105915407ead + * location-manager: Unref dbus proxy + * bt-manager: Unref dbus proxy + * wwan-mm: Unref modem dbus proxies + * wwan-mm: Simplify chain up. + Use same pattern as elsewhere + * lockscreen-manager Unref session-presence proxy + * screenshot-manager: Don't hold ref when acquiring the bus. + Otherwise we never dispose since the ref would only be dropped + on g_bus_unown_name which is called in dispose. + Since the manager stays around until shell shutdown this isn't a + problem. + * location-manager: Don't hold a ref during name watching. + Since the unwatch happens in dispose only we'd never drop that ref. + Since the manager stays around during the shell's lifetime this + is not a problem. + * torch-manager: Cancel proxy creation on dispose. + This makes sure there's no lingering async callbacks. + * monitor-manager: Adjust DBus naming. + Use DBus instead of Dbus as with all other protocols and drop + use PhoshDBus as namespace which we want to use for all protocols + that don't need an object manager. + Reindent the interface function arguments while at that. + Fixes: a749990f0c37c7e361f6c1df945053b0432d0415 + * mode-manager: Avoid warning when DBus operation got cancelled + * tests: Fix alphabetic ordering + * testlib: Simplify getting a useful shell object. + This adds functions to get a compositor, shell and isolated + session bus for testing which allows us to deduplicate some + code. + * test-notify-manager: Use testlib helpers. + Simplifies the code. + * test-monitor-manager: Use testlib helpers + * test-screenshot-manager: Use testlib helpers + * calls-manager: Move state enum to header file. + This makes it usable in the tests + * calls-manager: Don't clear calls hash when name owner goes away + gio's object-manager sends us proper remove signals for each object. + * gcovr: Exclude subprojects from coverage. + We don't bother about glib or gvc test coverate + * Add support for using glib as a subproject. + This allows us to run the tests that have our object manager fixes. + * gitlab-ci: Run extended tests on bullseye. + Don't bother for the crossbuild since we don't run tests there and this + would just make build times longer. + Since we don't want to build documentation for glib we need to do an + extra build for gtk-doc that uses system glib. + This gets simpler again once the next glib is released. + * calls-manager: Add test. + We need to preserve DISPLAY for xvfb and set XDG_RUNTIME_DIR since + g_test_dbus_up unsets those via g_test_dbus_unset. + This needs https://gitlab.gnome.org/GNOME/glib/-/merge_requests/2120 + to not deadlock. + * build: It's phosh, not libhandy + * phosh-wwan: Return NULL not boolean. + The operator is a string. + * check-po: Check for ui files too. + Use an extended regexp so we don't miss ui files + * po: Add gtk-mount-prompt.ui + * po: Use wildcard to ignore subprojects. + This makes sure we don't have to catch up when the subproject changes + * docs: Add calls DBus protocol + * HACKING: document that we prefer css names over style classes + * lockscreen: Drop style class. + We use gtk_widget_class_set_css_name so don't need an additional + style class. + * lockscreen: Use enum for page names. + It quickly becomes confusing otherwise + * lockscreen: Fix indentation. + Bring this in line with current coding standards. + * lockscreen: Use g_clear_handle_id () + + [ Sebastian Krzyszkowiak ] + * Revert "data: Update adaptive app defaults" + The commit intended to update adaptive app list, but it actually + changed the default favorites. + This reverts commit 19ebe2ec3a0df741b6935ac37de610b3c5e01117. + * data/phosh.service: Add Phosh to XDG_CURRENT_DESKTOP. + Commit 86dad1dfcc20b56add8a1dff5c4637ce51a0ec69 has already done + that for the Wayland session, but missed adding a similar change + to the systemd service. + * style: Adjust search bar spacing. + This brings it back to how it looked before introducing + app filtering, which was closer to the mockups. + + [ Vittorio Monti ] + * po: Update Italian translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Anders Jonsson ] + * po: Update Swedish translation + + -- Guido Günther Mon, 12 Jul 2021 16:01:37 +0200 + +phosh (0.12.0) byzantium; urgency=medium + + [ Guido Günther ] + * data: Add gsd-wwan to required components. + This makes sure it's also started in systemd mode so the PIN request + happens. + * tests: Remove smoke test. + It served us well initially but since we can now tests against + the headless compositor without extra setup. + * overview: Don't set CSS name twice + * protocol: Sort wayland protocols alphabetically + * phosh-wayland: Sort wayland protocol headers alphabetically + * shell: Sort manager objects. + Sort the ones that get created by the shell and those created on the fly + into separate sections and sort those alphabetically. Ideally we want + to distinguish the getters by name at some point. + (There's a third type (singletons) which we don't bother about here and + which we're reducing bit by bit) + * phosh-wayland: Get virtual-keyboard-manager. + This is only used in the tests. + * tests: Allow to inject virtual keyboard events. + That's fairly minimal at the moment but sufficient to test + dialogs, etc. + * system-modal-dialog: Allow to get and remove buttons. + Add API to query the current buttons and remove them. This just + proxies to GtkContainer. + * Implement most parts of org.Gtk.MountOperationHandler. + This allows to mount e.g. LUKS encrypted volumes via nautilus. + (Closes: #158) + * data: Add Phosh to DestkopNames. + This adds 'Phosh' to XDG_CURRENT_DESKTOP + * data: Only apply gsetting overrides to Phosh session. + This makes sure we don't e.g. messup settings for GNOME like + enabling the OSK. + * screenshot-manager: Remove opaque handler in dispose + * screenshot-manager: Actually set fader_id. + It got cleared but never set. + * lockscreen-manager: Shorten property enum values. + This is what we recommend in Hacking.md. + * proximity: Fix function spacing + * lockscreen: Drop style class. + We set it in the ui file already. + * Drop HANDY_USE_UNSTABLE_API. + Libhany's API isn't unstable anymore + * Add calls manager. + This tracks ongoing calls via the sm.puri.Calls DBus interface. + This can be used to (e.g. activate the proximity sensor and to + unblank the screen on incoming calls). + * lockscreen-manager: Unblank on incoming calls. + Do it here instead of in lockscreen-manager since + is where we'd also switch to the calls page later on. + * proximity: Only use proximity sensor on active calls. + See https://source.puri.sm/Librem5/calls/-/issues/175 + * debian: Add breaks relationship on older calls. + This makes sure calls we don't try to run against a calls without + the DBus interface. + * libhandy API isn't unstable anymore. + So drop HANDY_USE_UNSTABLE_API. + * overview: sort includes + * shell: Shorten property names. + This is what we recommend in HACKING.md + * overview: Add missing G_{BEGIN,END}_DECLS + * app-grid: Unref sorted model. + The following gtk_filter_list_model_new() takes a ref on its own. + * gitlab-ci: Build against PureOS byzantium. + Debian currently lacks recent enough kernel headers + * data: Always set `hidden_under_systemd` + This avoids a meson warning + * build: Set include dirs explicitly. + Otherwise we might end up with the toplevel include dir pretty late in + the search path and pick up GVCs config.h. + * build: Drop -I. from project_arguments + root_inc handles this for us. + * hks-manager: Fix typo + * hks-manager: Don't assert but rather use unknown operation type. + Getting odd info from /dev/rfkill is not worth crashing the shell. + * build: Write config.h later. + This allows us to use more results from feature detection. + * hks-manager: Use rfkill_event_ext when available. + This makes sure we parse two consecutive results correctly. If the + header is not available fall back to rfkill_event which works correctly + on kernels that don't expose this field. + * debian: Build-depend on recent kernel headers. + These provide rfkill_event_ext + * hks-manager: Don't leak list. + Although we had a `_and_free()` function we didn't clean it up. + Instead of cleaning up within that rather use automatic cleanup. + * home: Reveal whole home-bar instead of only arrow. + This gives us 40px more vertical space when there's no open applications + and removes the somewhat odd looking black bar. + * home: Add getter for overview + * overview: Add getter for the app-grid + * app-grid: Allow to filter for adaptive apps only + * app-grid: Allow for a configured list of adaptive apps. + This will help to avoid frustration until apps are tagged as such. + * app-grid: Move signal enum above class. + This makes it match the proposed layout in HACKING.md + * app-grid: Allow to toggle adaptive app filtering via setter and property. + This allows other parts of the shell to turn adaptive app filtering on + or off. + * app-grid: Move object cleanup to dispose() + Otherwise we get a reference cycle between self->actions and + and self disposal which keeps the app-grid around a bit longer + and just long enough to make app-list-model emit another items-changed. + Since `priv->search` is then gone we fail the 'priv->search != NULL' + check in search_apps. + * app-grid: Add menu button to toggle adaptive app filtering. + Add a menu next to the search bar that allows to filter adaptive apps + * shell: Sync adaptive app filtering with docked mode + * Allow to toggle app filtering globally. + This allows to turn of adaptive app filtering completely. + * data: Update adaptive app defaults. + Add those from phosh's default set that aren't marked yet + but are adaptive and drop those that are and had a release. + * Add NEWS file + + [ ZenWalker ] + * brightness: 'g_warning' instead 'g_error' + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + -- Guido Günther Tue, 29 Jun 2021 09:56:16 +0200 + +phosh (0.11.0) byzantium; urgency=medium + + [ Guido Günther ] + * d/control: Mark build-deps as nocheck + * gitlab-ci: Avoid some build-deps for cross-builds. + They're not needed since we just check if the build works. + * system-promper: Cleanup properly. + Unref the prompter and make sure we don't unregister if not registered. + * idle-manager: Guard against disposing multiple times + * polkit-auth-agent: Indentation fixups + * polkit-auth-agent: Don't leak error in error path + * build: Use files() + This avoids having path prefixes in some places and not in others + and makes it simpler to use these from other directories. + * test-app-grid-button: Drop test for expected message. + We catch that in the output of the subprocess and it's a noop + when running with `-DG_LOG_USE_STRUCTURED`. + * d/control: Add dbus-x11. + Needed for the shell test + * build: add static phosh lib too. + This contains the whole phosh without main() allowing reuse in + tests. By using `both_libraries()` we can munge that into the + shared lib build and unconditionalize this without increasing + the number of targets. + * tests: Create shell object. + This creates the shell object making sure we create most of + the objects that aren't created in the idle callback() + * d/control: Add more schemas. + The tests bring up more of the shell now so we need those schamas + during tests too. + * tests: Make shell enter main loop. + This brings up all the manager objects + * gitlab-ci: Drop the one output smoke-test. + The test-shell unit test basically covers this. + * gitlab-ci: Use gcovr instead of lcov. + This creates a bit nicer reports but more importantly supports + a gcovr.cfg in _build/ so we can exclude autogenerated files + in the future. + This needs some hackery due to + https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=987818 + but since this only affects this project and not e.g. downstreams + it should be o.k. for the time being. + * Add gcovr configuration. + This allows us to exclude autogenerated files. It needs to go + toplevel for gcovr to find it. + * Bump glib requirement to 2.62. + This is in byzantium/bullseye and will allow us to use + g_clear_signal_handler() and friends. + * Use g_clear_signal_handler. + We can rely on glib 2.62 now. + * brightness: Use simpler g_clear_object + * mode-manager: Use correct type. + Unused so far so the compiler didn't spot it. + * home: Use correct type. + Unnoticed by the compiler since the type was unused so far. + * monitor: Use proper cast. + This helps detecting bad types. + * gnome-shell-manager: Run parent constructor first. + This makes it consistent with other classes + * media-player: Unconditionally dispose DBus connection. + The connection is independent from the signal subscription. + * system-prompter: Always unown DBus name. + Don't bother if we ever acquired it, it's important if we tried + (and hence got a > 0 id). + * idle-manager: Unown DBus name. + We should clean up properly on dispose. + * monitor-manager: Unown DBus name. + We should clean up properly on dispose. + * notify-manager: Unown DBus name. + We should clean up properly on dispose. + * screen-saver-manager: Unown DBus name. + We should clean up properly on dispose. + * gnome-shell-manager: Unown DBus name. + We should clean up properly on dispose. + * shell: Dispose keyboard_events past gnome-shell-manager + gnome-shell-manager invokes phosh_shell_remove_global_keyboard_action_entries() + when disposing it's keybindings. + * gnome-shell-manager: Properly free grabbed keybindings. + We shouldn't remove elements form the GList of keys while looping over + it. Fixing this makes sure we actually fee the entries (and is less + code). + * gnome-shell-manager: Handle DBus skeleton export error + * screensaver-manager: Dup lockscreen-manager. + We unref it on dispose so we need to take a reference. + * on_bus_acquired: Use proper cast with type check. + Seems this got copy pasted all around and should. + * location-manager: Unwatch name on dispose. + We don't want to be called after going away. + * location-manager: Unexport the interface + * gnome-shell-manager: Unexport the interface + * monitor-manager: Unexport the interface + * notify-manager: Unexport the interface + * screen-saver-manager: Unexport the interface + * screenshot-manager: Don't opencode g_clear_handle_id + * screenshot-manager: Unexport the interface + * session-manager: Unexport the interface + * lockscreen: Move player to center widget. + This makes the player centered with the indicators and clock + See https://source.puri.sm/Librem5/phosh/-/issues/543 + * screenshot-manager: Only flash screen on success. + Don't flash screen when taking the screenshot failed. + * shell: Remove some trailing whitespace + * tests: Use define for startup-timeout magic constant + * util: Add helper for the common async callback error checking. + A canceled operation error should not print a warning while all others + should. + * connectivity-info: Cancel nmclient acquisition on dispose. + Otherwise we might access already freed data. We reorder the ready + handler to not access self when the operation is cancelled. + * wifimanager: Cancel nmclient acquisition on dispose. + Otherwise we might access already freed data. We reorder the ready + handler to not access self when the operation is cancelled. + * session-manager: Cancel session registration on dispose. + Otherwise we might access already freed data. We reorder the ready + handler to not access self when the operation is cancelled. + * brightness: Cancel proxy creation on dispose. + This makes sure there's no lingering async callbacks. + * gnome-shell-manager: Don't ref self when trying to own name. + If the shell quits and the async g_bus_own_name didn't finish then + `dispose` will not be run with when a ref is held and the name won't be + given up. This prevents us from freeing up properly. + * monitor-manager: Don't ref self when trying to own name. + If the shell quits and the async g_bus_own_name didn't finish then + `dispose` will not be run with when a ref is held and the name won't be + given up. This prevents us from freeing up properly + * modem-manager: Cancel proxy acquisition. + Instead of holding a ref on self make sure we cancel the operation in + dispose. This allows the object to dispose properly. + * background: Cancel async operation on dispose + * rotation-manager: Cancel async operation on dispose. + Instead of holding a ref during the async operation we cancel it in + dispose. Otherwise we might not properly dispose the object while an + async operation is pending. + We shuffle the code around to not access self prior to checking if the + operation was canceled since it might not be existent anymore. + * wwan-mm: Cancel async operation. + This makes sure we don't use self although already disposed. Lower + warning to `g_message` to not trip up tests when mm is not present + or the operation gets canceled. + * Add PhoshManager. + This adds a common base class for managers that currently derive + from GObject. + * mode-manager: Use manager. + This ensures the idle callback gets removed on dispose. + * bt-manager: Move functions between class_init and init upwards. + Matches our recommended layout and needed for the next commit. + * bt-manager: Use manager. + This ensures the idle callback gets removed on dispose. + * torch-manager: Move functions between class_init and init upwards. + Matches our recommended layout and needed for the next commit. + * torch-manager: Use manager. + This ensures the idle callback gets removed on dispose. + * status-icon: Add idle_init virtual method. + This allows for easier idle setup and makes sure we properly + remove the callback. + * torch-info: Use status-icon's idle_init. + Instead of opencoding use the parent's class functionality which + also makes sure the handler gets properly removed. + * wwaninfo: Use status-icon's idle_init. + Instead of opencoding use the parent's class functionality. + * bt-info: Use status-icon idle_init. + Instead of opencoding use the parent's class functionality. + * docked-info: Use status-icon idle_init. + Instead of opencoding use the parent's class functionality. + * wifiinfo: Use status-icon idle_init. + Instead of opencoding use the parent's class functionality. + * home: Drop idle callback. + Binding to settings is save and won't trigger excessive object + creation. + * media-player: Cancel idle callback in dispose. + Cancel idle callback on dispose so we don't run it if we're already + disposed. + * Replace some g_warning()s with g_message() + Some hardware is not required so reduce the log level. This helps + tests and since those messages still end up in logs by default don't + make debugging harder. + * batteryinfo: Use g_autoptr + * polkit: better error message + * polkit: Lower warning when we fail to get session. + This is likely due to no polkit at all. + * Move DBus connect failures from warning to debug. + When the DBus service is not even there no need to print a warning since + the shell just disables that part. + This helps to have warnings enabled in tests. + * tests: Drain event loop in shell test. + This make sure there's no lingering async/idle callbacks + of disposed objects. + * screensaver-manager: Cancel idle callback in dispose + * screensaver-manager: Don't hold ref on self during g_bus_own_name. + This avoids cancellation and hence proper cleanup. It's also not + needed since we unown the name in dispose. + * wifimanager: Don't use warning when failing to register agent. + Lower to `g_message` to not trip up tests. It's not fatal anyway. + * wifimanager: Don't use warning when failing to connect to NM. + Lower to `g_message` to not trip up tests. It's not fatal anyway. + * wifimanager: Unref access point. + We need to disconnect the signals manually since + nm_device_wifi_get_active_access_point is transfer none. + * background-manager: Allow to retrieve backgrounds. + Used in tests. + * shell: Allow to retrieve background-manager. + Makes it more symmetric with other managers, used in tests only atm. + * notify-manager: Don't hold ref when owning dbus name. + We unown the name in dispose. Holding the ref prevents proper cleanup. + * test-shell: Simplify shell setup and tear down. + This makes it reusable in other tests. We don't move that to testlib + since that does not link against shell.c atm but stubs the shell object. + * test-shell: Add test with two outputs. + We need a 2nd fixture since setting up the 2nd output in the + test itself is too late, the compositor is already running. + * gitlab-ci: Drop two-output test. + We run that via ninja now making it easier to use during local + development. + * ui: Remove receive-default properties from check buttons + 'False' is the default anyway. + * system-modal-dialog: Drop unused dispose + * polkit-auth-prompt: Simplify by using template_callback. + Less code. + * polkit-auth-prompt: Use correct cast. + It's a label not an entry. + Fixes: 3b60ed8fa4d08c18978e4b90ddf29e1cfd537c2b + * polkit-auth-prompt: Drop receives-default. + Otherwise moving the focus via tab to cancel and hitting + activates 'ok' instead of 'cancel'. + Also drop the deprecated double-buffered property. + Fixes: 3b60ed8fa4d08c18978e4b90ddf29e1cfd537c2b + * system-prompt: Drop property descriptions. + We have doc strings which allow for more context and don't + end up eating space in the binary. + * system-prompt: Simplify by using template_callback. + Less code. + * system-prompt: Drop receives-default. + Otherwise moving the focus via tab to cancel and hitting + activates 'ok' instead of 'cancel'. + Fixes: f2feed05d89bddf440685589b3c5f85bc61bd5f6 + * network-auth-prompt: Drop receives-default. + Otherwise moving the focus via tab to cancel and hitting + activates 'ok' instead of 'cancel'. + Fixes: 5ec5a932461fad35b3a9830fba3b23abe2a97050 + * app-auth-prompt: Drop receives-default. + Otherwise moving the focus via tab to cancel and hitting + activates 'ok' instead of 'cancel'. + Fixes: 14a763b094630cd326b4d7b09a834cd0e15a5973 + * end-session-dialog: Drop receives-default. + Otherwise moving the focus via tab to cancel and hitting + activates 'ok' instead of 'cancel'. + Fixes: fb4c5e1d3493865be3d84d00c3dc9859ea9bf107 + * end-session-dialog: Set focus. + Make this explicit as in other modal dialogs + * end-session-dialog: Avoid creating a row. + Since we don't need to disable focus anymore we can just use + gtk_list_box_insert (). + * end-session-dialog: Make listbox insensitive. + The list of inhibited apps should just be avoided when using keyboard + focus however just setting can-focus = False on the listbox and the + rows only makes it swallow the cursor completely. Mark the widget + as insenitive instead. This dims the widgets in the listbox so + undo that via CSS. + With that using tab to cycle through the dialog works as expected (only + switches bestween the buttons). + * mode-manager: Don't use wl before we checked the async operation. + Otherwise this can cause problems when the async calls is cancelled + in dispose and PhoshWayland already got disposed since we'd trigger + recreating the singleton. + * wifimanager: Don't check type before async error check + self might already be gone if the async call got cancelled in dispose. + * docs: Drop coverage report. + Using a gitlab badge is nicer since it doesn't clutter the the docs. + * wwan: Move wwan DBus sources to src/dbus. + This gets them ignored when calculating coverage but also gets us closer + to have all DBus generation in once directory and meson variable. + We keep file and interface names unchanged for minimal diff noise and + just rename the interface definitions. + * monitor: Move DisplayConfig DBus sources to src/dbus. + This gets them ignored when calculating coverage but also gets us closer + to have all DBus generation in once directory and meson variable. + We keep file and interface names unchanged for minimal diff noise. + * osk: Move OSK0 DBus sources to src/dbus. + This gets them ignored when calculating coverage but also gets us closer + to have all DBus generation in once directory and meson variable. + We keep file and interface names unchanged for minimal diff noise and + just rename the interface definitions. + * osk: Move to src/ + No point to have a directory for single file. + * tests: testlib: Unregister timeout handler. + Otherwise it might fire at a later invocation. + * tests: testlib: Don't fetch outputs early. + This can cause trouble when using this to bring up the shell since we + might otherwise miss wayland events in the shell (see + phosh_shell_constructed). + * tests: Always run idle-manager test. + Now that we can use PhoshShell in tests use that + in a thread plus a spawned compositor to always + run the idle tests instead of requiring a running + shell upfront. + For that we need to fix resource leaks to not reuse + DBus proxies on already gone shells, etc. + We mark DBus session bus using tests as not being run in parallel for + now since test clients might otherwise get confused. + * tests: Add initial screenshot-manager test. + Takes a screenshot and makes sure it ends up in the right location + on disk. + * Add initial monitor-manager test. + Just queries the current state. + * tests: Add initial notify-manager test. + Since we want notifications on the lock screen soonish we better + test this a bit. + * gitlab-ci: Use libglade-common from experimental too. + This fixes the build until bullseye gets unfrozen + * po: ja: Fix printf format in translation. + It swapped to arguments without marking as such. + * po: ja: Remove second plural forms. + Japanese uses nplurals=1 + * gitlab-ci: Install gettext. + Needed for po file check + * Move po check from gitlab-ci to a tools/ + This allows for easy local checking + * tools/check-po: Check po files via msgfmt. + This makes sure we don't end up with incorrect plural forms + or broken format strings. + * top-panel: Indicate disabled state for wifi and bt. + The top panel so far handled disabled the same as missing for wifi and + bt. + * settings: Add torch brightness slider. + This adds brightness slider when the torch is enabled. (Closes: #386) + * debian: Switch to debhelper 13. + This makes us fail on missing files + * hacking: Fix property function names. + We always use the singular form + * Add css names to info widgets. + This eases styling and makes things consistent with other widgets + * top-panel: Add css name. + Instead of using a CSS class on the top-panel and the widget name + for home make this consistent. + We use 'top-panel' since this is what is should be named (pending + a source file and class rename). + * top-panel: Always use bold font for indicators. + We always want to use bold font on the right indicator box + * batteryinfo: Drop superfluous NULL checks + g_clear_object() does this for us. + * batteryinfo: Add optional label with battery percentage + * panel: Honor show-battery-percentage. + This allows to show the battery percentage in the top bar + Closes: https://source.puri.sm/Librem5/phosh/-/issues/268 + * debian: Use systemd. + Use systemd for the CI debs and simplify installation (we don't + need any enablement) + * notify-manager: Fix docstring + * notifications: Expand docs a bit. + I always need to look up the relationship of the classes. Add some + docs to make this simpler. + * notification-content: Make sure we disconnect on dispose. + Use g_signal_connect_object() instead of g_signal_connect() + so that if self goes away the signal handler gets disconnected. + The notification might live way longer than the content, e.g. when a + banner expires but the notification is still in the notification list. + Closes: https://source.puri.sm/Librem5/phosh/-/issues/321 + * shell: Emit signal when shell is fully up + * tests: Test shell ready + * main: Measure startup time + * main: Inform systemd we're up + * systemd: Inform systemd we support notify. + Closes: https://source.puri.sm/Librem5/phosh/-/issues/568 + * tests: Don't hardcode version + + [ ZenWalker ] + * layersurface: avoid redundant assignment + * end-session-dialog: avoid redundant assignment + + [ Pablo Correa Gómez ] + * settings: Remove duplicated assignment after check + + [ Dylan Van Assche ] + * proximity: faster proximity fading. + Fades out faster when in proximity. + This improves the user experience when placing a call + as the phone blanks faster. The user isn't 'afraid' + anymore to accidentally touch a button with + his/her/their ear when placing a call for example. + + [ lajonss ] + * settings.c: quick settings Wi-Fi and BT toggle. + Quick settings Wi-fi and BT buttons + were opening the corresponding settings panel. + This commit introduces new behaviour for + Wi-Fi and BT quick settings: + - toggling enabled state on clicked event + - opening settings panel on long_pressed event + This commit introduces "button-pressed" feedback + on opening settings panels. + Concerns: #372 (Quick toggles open settings panels) + * settings: wwan quick toggle. + Changes behaviour of wwan quick settings from: + - opening settings panel on click, + to: + - toggling wwan enabled on click, + - opening settings panel on long press. + Introduces PhoshWWanManager - base class for WWan + backends. Used for backend-agnostic nm operations. + Concerns: #372 + + [ Zhaofeng Li ] + * data: Generate required session components from an array + * Add systemd user units. + This allows Phosh to be started with `gnome-session --systemd`, with + most session components managed by systemd. + * Allow the compositor path to be overridden in a Meson option. + However, `/usr/bin/phoc` will still be used if it exists. + * Add environment variable to support overriding the gnome-session executable + + [ Vittorio Monti ] + * po: Update Italian translation + + -- Guido Günther Sun, 30 May 2021 12:22:27 +0200 + +phosh (0.10.2) byzantium; urgency=medium + + [ Sebastian Krzyszkowiak ] + * docked-manager: Check whether docked mode can be enabled. + + [ Guido Günther ] + * dbus: Fix indentation + * Add screenshot manager. + This, for now, just captures the primary display which is the main + use case on mobile. + * css: Fix indentation + * system-modal-dialogs: Add box shadow. + This makes sure the dialog gets visually distinct e.g. on dark-mode + apps. + Closes: https://source.puri.sm/Librem5/phosh/-/issues/538 + * proximity: Make sure we destroy the fader when we unclaim the sensor. + We must never blank screen when not tracking proximity. + * home: Fix use after free + * system-modal: Drop unused struct member + * fader: Simplify fader. + No need to bother all layers with the wayland details, just + pass in a monitor. + * fader: Allow to set style class. + This allows to use animations of any kind. + * screenshot-manager: Copy to clipboard if filename is empty. + * screenshot-manager: Use fader for screen flashing + * Various style and consistency cleanups + + [ Mohammed Sadiq ] + * meson: Fix build with tests disabled. + * panel: Hide popover before folding top panel. + Otherwise the popover shall re-orient to adapt with the parent + widget before it's hidden + * style: Use tabular font for clocks. + So that the text has same size allocated and thus the text won't move + when the value changes, which is very much visible when clock shows + seconds + * top-panel: Update power button style to match design + + [ Anders Jonsson ] + * po: Update Swedish translation + + [ Evangelos Ribeiro Tzaras ] + * monitor-manager: fix typo. + + [ scootergrisen ] + * po: Update Danish translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Daniel Șerbănescu ] + * po: Update Romanian translation + + [ 寮 ] + * po: Update Japanese translation + + -- Guido Günther Tue, 27 Apr 2021 10:21:21 +0200 + +phosh (0.10.1) byzantium; urgency=medium + + [ Guido Günther ] + * rotateinfo: Fix indentation + * monitor-manager: Fix doc string. + Make function name match the actual definition. + * torch: Make debug statement more useful. + We want to what things changed not only that s.th. happened. + * proximity: Handle NULL builtin monitor. + There might be no builtin monitor at all or it could be disabled. + * shell: Create sensor-manager early. + We want that before the panels since those might use other + managers that need the sensor-manager (e.g. rotation-manager) + * shell: Don't bother with accelerometer. + Rotation-manager takes care of that + * shell: Decruft builtin monitor setup. + Just look it up on start since it can't change. + * Avoid sensor-proxy singleton. + Treat it as regular initable that is fetched from the shell + like other managers. + * Add rotation-manager. + The rotation manager listens to device orientation changes + and adjusts the primary display accordingly. + This interfaces with lockscreen, iio-sensor-proxy and + gsettings to figure out the correct screen orientation. + The manager has two modes: off (don't rotate any output) and sensor + (adjust due to sensor values). + * rotateinfo: Display either rotation lock or orientation. + Based on the rotation managers mode display appropriate information. + (Closes: #18) + * settings: Add orientation lock to rotate quick setting. + Long press switches between portrait/landscape and rotation lock. + * settings: Don't close menu when toggling rotation lock. + This makes it consistent with other toggles like feedback toggle. + * Move Lockscreen rotation fixup to rotation-manager. + No need to have several objects involved. This avoids + lockscreen and rotation manager racing since we can + do it past the unclaim of the accelerometer. + * rotateinfo: Use monitor from rotation-manager consistently. + So far this was dependent on mode and incorrectly tracked the primary + monitor for manual toggle. + * shell: Drop transform handling. + This is done by the rotation-manager which always acts on the + given monitor. (Closes: #56) + * rotation-manager: Mess with transform as little as possible. + On phones when we're already using portrait orientation don't bother + fixing this up. This allows for e.g. upside down operation. + * monitor: Update org.gnome.Mutter.DisplayConfig. + This is from mutter's 331b5f356311f1dcfc1b580e349a60d25fc0e34f + * monitor-manager: Drop supports-mirroring prop. + It got removed with the DisplayConfig interface update. Mutter doesn't + use it anymore, g-c-c still parses it but doesn't use it in 3.38. + * monitor-manager: Handle panel-orientation-managed (Closes: #540) + * monitor-manager: Delay 'monitor-added' until monitor is configured. + This makes is simpler for other parts of the shell since they don't + need to track this on their own. + While at that make phosh_monitor_manager_add_monitor private since + this should only happen within monitor-manager. + * shell: Handle builtin monitor dynamically. + It e.g. goes away when disabled in docked mode + * rotation-manager: Update tracked monitor. + Update tracked monitor when builtin changes. We do this explicitly + rather than listening to changes on monitor-manager since this way + the rotation-manager doesn't need to be aware that it's tracking + a built-in monitor. It just tracks what's passed in. + * rotateinfo: Make insensitive when built-in monitor is disabled + * end-session-dialog: Drop debug leftover + * lockscreen-manager: Remove unused timeout property. + Unused since d3028d50686a1fba7876355ea3272def281ad030 + * lockscreen-manager: Document phosh_lockscreen_manager_set_locked. + Use a better argument name while at that. + * lockscreen: Fix signal name + * monitor-manager: Clarify doc string + * screen-saver-manager: Clarify debug message. + Make it obvious we're sending out the DBus signal + * shell: Look at the right manager. + Don't check torch manager when we want location. + * shell: Don't create loctation manager twice. + Since the panels are constructed before the init in the shell's + idle_cb we'd construct twice since priv->location_manager is still + NULL. USe the getter instead to be independent from init order. + * location-manager: Translate location accuracy levels. + Geoclue uses something different then the desktop schemas. + Thanks to Dylan Van Assche for the report + + [ Clayton Craft ] + * meson: bump version to 0.10.0. + Seems like this was missed when 0.10.0 was tagged. + + [ Daniel Șerbănescu ] + * po: Update Romanian translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Emin Tufan Çetin ] + * po: Update Turkish translation + + [ Jan Jasper de Kroon ] + * po: Update Dutch translation + + -- Guido Günther Sat, 10 Apr 2021 09:18:33 +0200 + +phosh (0.10.0) byzantium; urgency=medium + + [ Guido Günther ] + * osd-window: Drop padding around levelbar. + This avoids a thin black border noticeable on light backgrounds + * wwan-iface: Add 'enabled' property. + This indicates if the modem is enabled. + * wwan-mm: Complete PhoshMMModemState. + This matches MMModemState + * wwan-mm: Handle 'enabled' property + * wwan-ofono: Handle enabled property. + We always return TRUE here to not break existing users. + * wwaninfo: Correctly indicate disabled state. + So far we used the signal-strength 0 icon which is confusing. + * Add geoclue DBus interface files. + This were taken from gnome-shell as of + 1dc971d76025416249598f9b8e5a12e37c9dec22 + * Add geoclue location manager. + This honors the 'org.gnome.system.location enabled' GSetting for global + access to location services. + To work it needs an entry for phosh in /etc/geoclue/geoclue.conf. + (Closes: #153) + * Add "location service in use" indicator. + This indicates that the location service is in use via the status bar. + (Closes: #527) + * location-manager: Reject all per app auth requests. + Reject auth by default for the per-app case until we implemented per app + location permissions (#524). + It currently doesn't hurt us badly since the current test in geoclue + whether there should be an auth request is based on + gclue_client_info_get_xdg_id() returning !NULL. Due to /proc/%u/cgroup + not containing '1:name=systemd:' this isn't the case and hence apps + are authorized by geoclue by default. + * polkit-auth-prompt: Make buttons use the whole width + * polkit-auth-prompt: Move auth button creation into ui file. + This makes it simpler to maintain with the reset of the code + * polkit-auth-prpmopt: Move info label next to icon. + This makes it more consistent with the other dialogs. + * polkit-auth-agent: Make buttons a bit larger. + This makes them easier to use on mobile. We add a style class + so we can reuse that in other modal dialogs. + * style.css: Move spacing to content area. + This allows buttons to go to the very bottom of the dialog box. + * system-modal: Document the style classes + * system-prompt: Use system-modal style classes + * network-auth-prompt: Use system-modal style classes + * system-modal: Use ultrabold font for title. + Introduce a style class for that. The other dialogs + need more work since they don't use a generic title yet. + See #201 + * top-panel: Add some spacing between the entries. + Helps #331 + * polkit-auth-prompt: Remove the button border. + This gets this dialog in line with the buttons of other modal dialogs. + * system-modal: Adjust button layout to designs + * system-modal: Adjust dialog color to designs (Closes: #201) + * system-prompt: Adjust to designs. + Drop the icon and center message texts + * network-auth-prompt: Adjust to designs. + Drop the icon and center message texts + * system-prompt: Add missing G_{BEGIN,END}_DECLS + * app-auth-prompt: Used to authorize app permissions + * location-manager: Use app-auth-manager to ask for geoclue permissions + (Closes: #524, #532) + * dir-locals: Don't use tabs in ui files + * test: Move get_monitor() to testlib. + Useful for other layer-surface tests as well + * Add system-modal-dialog class. + This can serve as base for system modal dialogs + * polkit-auth-prompt: Use PhoshSystemModalDialog + * polkit-auth-prompt: Use SystemModalDialog's `dialog-canceled` signal + * polkit-auth-prompt: Adjust to designs. + Center text and icons. We need to use a label instead of + putting the placeholder text into the password entry since + GTK3 only allows for a placeholder text when the entry does + not have the focus. + * system-modal-dialog: Allow to swipe away. + Allow to cancel dialogs by swiping them away + * system-modal-dialog: Limit width to 400px. + Add a clamp so the dialog keeps a sane width to height ratio. + * app-auth-prompt: Use system-modal-dialog (Closes: #536) + * system-prompt: Switch to system-modal-dialog. + Less code and more consistency. + * network-auth-prompt: Switch to system-modal-dialog. + Less code and more consistency. + * network-auth-dialog: Remove unused system-prompt-grid. + Takes up vertical space but isn't used. + * style.css: Remove now unused styling. + All dialogs use PhoshSystemModalDialog now + * batteryinfo: Don't leak string + * head: Don't reassign product. + Use model as intended + * system-modal-dialog: Tweak system-modal-dialog CSS + - Use slightly darker background + - Use 2px separator between buttons + - Use 2px separator to dialog content + See: #537 + * dir-locals: Use 2 spaces for css indent + * gnome-shell-manager: Properly name variable. + Looks like a c'n'p from PhoshNotifyManager + * docs: Add PhoshSystemModalDialog + * app-list-model: Prefix timer with [phosh] + This makes it consistent with other timers + * dbus: Add org.gnome.SessionManager.EndSessionDialog protocol. + This will be needed for the 'end session' dialog + * panel: Drop workarounds + * tools: Add script to check end-session-dialog + * Add PhoshEndSessionDialog. + This dialog will be invoked by PhoshSessionManager to present + logout/reboot/restart requests. + * session: Export end-session-dialog. + Export the end-session-dialog DBus API used by gnome-session. Upon + invocation it will spawn PhoshEndSessionDialog. (Closes: #54, #520) + * home: Don't reuse bindings + g_settings_get_strv() is transfer-full so reusing it before freeing + it beforehand leaks the data. + * background: Don't leak slideshow. + We need to unref it before reassigning a new one. + * gnome-shell-manager: Don't leak GVariantIter + + [ Evangelos Ribeiro Tzaras ] + * shell: Use G_PARAM_STATIC_STRINGS for all properties + * phosh_shell_get_state: Use enum value instead of magic constant + * phosh_shell_get_state: Use `Removing from` instead of `Removing to` in + debug message + * shell: make `shell_state` a property. + This allows to bind the new GnomeShellManager:action_mode property. + For that we move the initial call to `phosh_gnome_shell_manager_get_default` + to `PhoshShell`'s `setup_idle_cb()` (which is nicer anyway since we do + less work on startup). + Closes #513 + + [ Sebastian Krzyszkowiak ] + * style.css: Disable PhoshActivity's button transitions. + Adwaita sets `transition: all 200ms $ease-out-quad;` for all + GtkButtons. In our case, the transitions being animated are + not visible at all, but they still cause redraws, which can + be rather heavy when window thumbnails are involved. + + [ Andy Holmes ] + * screen-saver-manager: fix type name in idle callback. + Type name was PhoshTorchManager instead of PhoshScreenSaverManager + + [ Jan Jasper de Kroon ] + * po: Update dutch translation + + [ Emin Tufan Çetin ] + * po: Update Turkish translation + + [ Jaroslav Svoboda ] + * po: Update Czech translation + + [ Balázs Meskó ] + * po:Update Hungarian translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + -- Guido Günther Tue, 30 Mar 2021 16:02:47 +0200 + +phosh (0.9.0) byzantium; urgency=medium + + [ Sebastian Krzyszkowiak ] + * PhoshHome: Hide overview when fully folded. + This prevents useless rendering work from happening offscreen on window + focus or activator list changes. + * PhoshHome: Fix jumping position when reversing an active animation + `1.0 - self->animation.progress` assumes linear interpolation; however, + cubic ease out is used for animating home view position, so the new + reversed position needs to be adjusted for that in order to start from + the same place on the screen. + + [ Evangelos Ribeiro Tzaras ] + * minor: Fix typos, indentation and code style issues + * shell-manager: Honor action modes + * lockscreen: Set shell state. + And allow querying the shown page + * gnome-shell-manager: Distinguish lock and unlock screen + * gnome-shell-manager: Pretty print grabMode mismatch + * shell: Pretty print shell state changes + * prompts: Update PhoshShellState + * home: Update PhoshShellState + * shell: screen blanking: Update PhoshShellState + + [ Guido Günther ] + * settings: Ref output stream. + The 'default' in gvc_mixer_control_get_default_sink () made + me believe the object would stick around but it doesn't so + we need to hold a ref. (Closes: #510) + * Add PhoshHksManager. + This tracks the state of hardware kill switches (rfkill + hardblocked devices) + * shell: Add PhoshHksManager. + RFKILL code heavily borowed from g-s-d + * data: Add icons for disabled camera and mic. + These can be dropped once we can rely on GNOME 3.38. + * Add PhoshHksInfo. + This can be used to display info about the different HKS devices + * top-panel: Add camera and microphone hks info + * ci: Drop support for Debian buster and PureOS amber-phone. + Drop support for Debian buster/PureOS amber-phone since we can't + sensibly support these for CI builds since the packages in buster + are too old and amber-phone lags so we have to wait too long for + migrations. + amber-phone will be handled via pkg-phosh. Bullseye will switch + to bookworm once that's open. + * gitlab-ci: Drop CI repo remanents. + It's not being filled since ages + * gitlab-ci: Fetch libhandy-1 from Debian experimental. + We'll need this until either Byzantium catches up or Bookworm opens + * dbus: Shell: Add showOSD and add stub + * Add PhoshOsdWindow. + This finally adds OSD information. (Closes: #33) + + [ Pierre Michel Augustin ] + * po: Add Creole Haitian translation + + [ Alexander Mikhaylenko ] + * Bump libhandy version to 1.1.90. + This will be required to use #HdyCarousel:allow-long-swipes. + * overview: Use long swipes. + Allow to swipe multiple pages at once. + + -- Guido Günther Tue, 02 Mar 2021 14:52:00 +0100 + +phosh (0.8.1) amber-phone; urgency=medium + + [ Guido Günther ] + * shell: Simplify css loading + * shell: Don't use magic constant for CSS provider priority + * settings: Ensure types in shell. + We use widgets at various places. Make sure we make the types known + in a single location since otherwise there's duplication. + * panel: Ensure types in shell. + We use widgets at various places. Make sure we make the types known + in a single location since otherwise there's duplication. + * lockscreen: Ensure types in shell. + We use widgets at various places. Make sure we make the types known + in a single location since otherwise there's duplication. + * shell: Move type setup to class_init. + Doing it on the class level is enough. The shell is a singleton + so this doesn't change anything in practice. + * shell: Always use g_type_ensure. + Older code used `_get_type ()` + * top-panel: Remove duplicate properties + * top-panel: Untabify. + It was a mixture of tabs and spaces + * ui: Untabify all files. + We want spaces there. The top-panel was the first offender + but let's get the others correct too. + * service: Always restart + gnome-session prefers exit status 0 - even on errors. So restart in this + case too. + * monitor: Add getter for wl_output + * build: Move monitor sources to tool's lib. + This allows usage in tests and tools. This will be needed by + the system-modal tests. + * notification-banner: Always use primary output. + This makes sure notification banners end up where the top bar is. + * layersurface: Don't make props construct-only. + This allows us to override in the derived classes constructor. + * Add PhoshSystemModal. + Abstract class for system-model dialogs. This allows all system modal + dialogs to use the same base class avoiding leaking layer-surface + details up into the class that initially builds the dialog. + * system-modal: Add minimal tests + * system-prompt: Use PhoshSystemModal + * polkit-auth-prompt: Use PhoshSystemModal + * network-auth-prompt: Use PhoshSystemModal + * network-auth-prompt: Focus password entry. + Similar in spirit to 0098cdfc9d791b56f580cc76ea1f8e42a31efbdb + * system-modal: Add style class. + This allow to remove it from 3 ui files + + [ Sebastian Krzyszkowiak ] + * gitlab-ci: Use http URI for amber-phone repos. + Otherwise initial apt-get update fails because of missing ca-certificates. + * settings: Add a long-press shortcut to g-c-c display panel on "Docked" icon + + [ Elias Rudberg ] + * Add toplevels_pending array and keep toplevel pointers + (Closes: #407) + + -- Guido Günther Mon, 08 Feb 2021 20:43:29 +0100 + +phosh (0.8.0) amber-phone; urgency=medium + + [ xam ina ] + * po: Update French translation + + [ Guido Günther ] + * keyboard-events: Drop newlines from `g_warning()` + g_warning newline terminates on it's own. + * d/control: Update from debian. + This typos and package sections + * d/control: Update dependencies/recommends from Debian. + We don't pull them in on amber and we want them for byzantium + anyway. + * gitlab-ci: Drop test-package stage. + Since we have PureOS packaging mostly separate drop the + last pipeline stage since that is run there. This brings + down build times and uses fewer runners. + * Move overrides to data. + This makes it simpler for downstreams to track changes. + * Move systemd service to data/ + This makes it simpler to track for downstreams. + * lockscreen: Drop leading space in day of month (Closes: #225) + * po: Adjust locales to string change. + This avoids breaking all locales + * notify-manager: Don't leak notification. + A ref is taken when added to the list so no need to keep + it around + * settings: Make sure we rotate clockwise + wlroots as of 0.11.0 changed rotation correction. Adapt to that, + we don't bother what older versions do - downstream can just + revert. + * shell: Properly sync lock property. + The shell's locked property did not really reflect the state of + lockscreen-manager's "locked" property. Fix this by binding those two. + This will also simplify things like the rotation manager since we + don't need to pass the lockscreen-manager around anymore. + * polkit-auth-agent: Disable verbose debugging. + We don't want cookies, etc to end up in the logs + * polkit-auth-prompt: Unref session too. + * polkit-auth-agent: Toggle auth prompt with lockscreen visibility + * wifimanager: Toggle network auth prompt with lockscreen visibility + (Closes: #299) + * system-prompter: Toggle system auth prompt with lockscreen visibility + * monitor-manager: Clarify physical and logical monitors + * monitor-manager: Use head for physical monitors in get_current_state + * monitor-manager: Don't bother setting up modes in GetResources. + Only the ones from CurrentState are relevant. + * monitor-manager: Make sure to bump serial on config changes + * monitor-manager: Notify DBus listeners about config changes + * monitor: Add description. + We'll use that to match up with heads + * head: Add connector information. + Needed to switch monitor-manager to use heads. + * Allow to switch output configurations. + This allows to disable heads and turn them back on which is useful for + docked mode) + * monitor-manager: Flip transform. + The transform we get from the DBus protocol is flipped + regarding Waylands view on it. This is similar to what + was done in wlroots to preserve config changes. + * protocols: Update wlr-output-management + v2 supports make/model/serial. + * head: Use v2 of wlr-output-management. + This allows for vendor/product/serial again. + * monitor-manager: Improve display name. + We have the necessary info in the heads now. Code + inspired from what's done in mutter. + * head: Add a mode name and lookup. + We don't bother with a hash map since e.g. removal would mean + iteration too so just do a linear search when needed. + * monitor-manager: Set output resolution as well (Closes: #140) + * head: Allow to fetch supported scales. + We only do integer scaling and use the same limits than mutter + (max 400%). + * monitor-manager: Allow to set scaling too + * mode-manager: Improve external output detection. + If there's a single output and it's not built-in it's an external + one. (Closes: #436) + * head: Allow for smaller logical area in portrait. + This allows to set the scaling e.g. on the Librem 5. + * head: Allow to easily clear all pending state. + Instead of having this in different places add a single helper. + * polkit-auth-prompt: Grab focus on every request. + Otherwise the input field isn't refocused after a failed attempt. + (Closes: #470) + * system-prompter: Focus confirmation entry. + Select the confirmation entry when showing a confirmation prompt. + * system-prompter: Focus password entry. + Refocus the password entry when the prompt is shown. Otherwise + the focus is on the button after failed password attempt when + using touch. + * timestamp-label: Introduce phosh_time_diff_in_words. + This is mostly to make things easily testable + * timestamp-label: Unbreak relative days and months. + Phones have a long uptime so fall back to pure date printing + after 1y. + * timestamp-label: Unbreak relative years. + So far there always was fallback, drop that. + * timestamp-label: Add more flexibility to formats. + Some languages want to have 'almost' and 'over' past the numbers. + * po: Update pot file + + [ Anders Jonsson ] + * po: Update Swedish translation + + [ Tomasz ] + * Update Polish translation + po: update Polish translation + + [ Timo Jyrinki ] + * po: Update Finnish translation + + [ Sebastian Krzyszkowiak ] + * gitlab-ci: Don't use global before_script for package jobs. + It used to work accidentally because pipeline definitions + had been overriding before_script, which isn't the case anymore. + * gitlab-ci: Simplify package:deb-pureos-amber job. + Thanks to librem5-ci!14 our before_script doesn't have to + do the apt preparations anymore. + * gitlab-ci: Use "needs" keyword instead of "dependencies" for test+docs stage. + This allows for better parallelism in the pipeline. + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ 寮 ] + * po: Update Japanese translation + * docked-info: Mark 'Undocked' as translatable too + "Docked" and "Undockd" are user visible strings and + only the former was marked as translatable so far + + -- Guido Günther Sun, 17 Jan 2021 17:46:56 +0100 + +phosh (0.7.1) amber-phone; urgency=medium + + [ Guido Günther ] + * background: Correctly chain up in dispose. + This fixes a crash when unplugging a 2nd screen. + Fixes 8828176f6c98a4bde94c9f6daa46d0a87a3c8d20 + * Add initial stubs for org.gnome.Shell DBus protocol. + This will be needed for OSD, monitor labels and grabs + * activity: Allow to focus. + This fixes the toggle-overview keybinding + Fixes 20c3a79fb888e876f30d1a590084c87cb1c16591 + * home: Hide arrow when there are no activities + (Closes: #261) + * Handle logind's Lock and Unlock. + This allow 'loginctl lock|unlock ' to work. + * phosh.service: Set session type and name. + This way we can match the right session via phosh_find_systemd_session + even when not using a window manager. We can drop the XDG_SESSION_TYPE + once phoc switches to newer wlroots which takes care of it since 0.12.0. + + [ Evangelos Ribeiro Tzaras ] + * shell-manager: Implement keygrabbing. + Ungrabbing is only allowed for the client who originally grabbed + * settings: get rid of explicit volume key handling. + With the DBus interface org.gnome.Shell now supported, g-s-d/media key + plugin now handles volume keys for us + + [ Sebastian Krzyszkowiak ] + * Remove custom drawing in system prompts. + Custom drawing doesn't seem necessary. Fixes unwanted background in + popovers. + + [ Arnaud Ferraris ] + * data: add 'System' category to desktop entries + + [ Andika Triwidada ] + * po: Add Indonesian translation + + -- Guido Günther Thu, 17 Dec 2020 12:46:29 +0100 + +phosh (0.7.0) amber-phone; urgency=medium + + [ Guido Günther ] + * Add session-manager. + This adds a proper object - current session.c doesn't have that + making it hard to use signals, etc. + * Add mount-manager. This automounts new volumes + * notify-manager: Add method to submit notifications. + This will allow to submit notifications from other parts of + the shell. Use it for the DBus part too. + * notification: Allow for a NULL timestamp. + This just uses the current time. + * timstamp-label: Use 'now' instead of <15s. + It looks odd when new notifications print '<15s' right away. + * notify-manager: Allow derived classes to use different action handlers. + We can turn the current 'default' notification into a + PhoshDBusNotification to make that even more pronounced at some point. + * Add PhoshMountNotification. + A notification that displays information about newly mounted + file systems. + * mount-manager: Indicate mounts via notifications. + We add a single action that opens the containing folder. + * mount-notification: Only add action if we have a handler. + Since nautilus is not adaptive don't add an action if + it's not available. + * Add PhoshDBusNotification. + This allows us to have notification just do the right thing. + * notification: Move do_action into dbus-notification. + This avoids the special case in the notification manager. + * data/phosh: Print version with `--version` (Closes: #425) + * run: Simplify oneshot gdb. + Often we just want to run phosh and get a backtrace e.g. + for G_DEBUG=fatal-criticals: + PHOSH_AUTO_GDB=1 SKIP_GNOME_SESSION=1 _build/run + does this. + * gitlab-ci: Make it simple to get a backtrace. + This can be done by passing $SMOKE_PARAMS='-c' to gitlab-ci. + * background: Add initial support for Background XML. + Many 'slideshows' are just a fixed single image so handle that + as a start. This allows us to use Debian's/PureOS background + data (as per desktop-base package) + * network-auth-prompt: Handle "sae" for WPA3 support. + This unbreaks authenticating to WPA3 networks which + so for ended in "not supported". + * shell: Only show banners for apps with show-banners on + + [ Evangelos Ribeiro Tzaras ] + * meson: bump version to 0.50 + + [ Alexander Mikhaylenko ] + * Slide back windows that failed to close. + Closes: #429 + * Use easeOutBounce interpolator for the slide back animation. + Make it a little fancier. + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + -- Guido Günther Thu, 10 Dec 2020 13:58:42 +0100 + +phosh (0.6.0) amber-phone; urgency=high + + [ Guido Günther ] + * run: Prepend current schema dir. + This allows phosh to e.g. find phoc's schema if that also uses run.in + * run.in: Use memory GSettings backend. + This makes sure we don't mess with systems settings. + Since this affects testing docked mode we allow to override it. + * gitlab-ci: Build bullseye packages as well. + This helps testing MRs against PureOS byzantium and Debian Bullseye + * d/control: Update package description + * debian: Add phosh-mobile-tweaks. + We have this in Debian and it's useful overall since it minimizes + librem5-base and makes it simpler to track changes for downstreams. + * Rename notify dbus interface class to PhoshNotifyDBus. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * Rename idle dbus interface class to PhoshIdleDBus. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * Rename screensaver dbus interface class to PhoshScreenSaverDBus. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * layer-surface: Add debugging when a surface goes away. + This helps tracing the order of events + * lockscreen-manager: Explain purpose + * lockscreen-manager: No need to disconnect signals from lockscreen. + We're about to destroy the lockscreen object a couple of lines later + * lockscreen-manager: Move lock screen when output goes away. + We move the lock screen in case the monitor it's on goes away. + (Closes: #385) + * monitor: Add phosh_monitor_get_power_save_mode. + Simple getter + * lockscreen-manager: Handle transform only on power mode changes. + This makes sure we rotate correctly when the screen unblanks and + we don't operate on disabled outputs which trips up phoc. + * lockscreen-manager: Don't rotate external screens. + We keep the transform there as well assuming the lock screen fits. + * home: Handle osk-button visibility. + This was currently split between the button and the home + bar which resulted in the button being shown although + it should stay hidden because the OSK was disabled. (Closes: #403) + + [ Alexander Mikhaylenko ] + * build: Compile gschemas on install + * overview: Enable carousel reveal animation + * feedback-manager: Stop using deprecated signals. + This will be useful when PhoshActivity stops subclassing GtkButton. + * Add PhoshAnimation. + This will help us to have less verbose code later. + * Add PhoshSwipeAwayBin. + Implement a simple widget for swipe-to-remove pattern. It's always + vertical and changes the child's position and opacity during the swipe. + * activity: Implement swipe-to-close. + Stop subclassing GtkButton, instead contain a PhoshSwipeAwayBin + and a GtkButton inside. Pass through the clicked signal from the button. + Make close button animate the swipe bin so that closing is animated too. + * activity: Show close button only on hover. + Now that we have another way to close apps on touch, let's hide the button + and show it only on hover. This essentially makes it pointer-only. + * Require libhandy 1.0.2 + + [ Arnaud Ferraris ] + * shell: add an accessor for the mode manager. + Other modules should be able to query the device type too. + * lockscreen-manager: undo transform only for phones. + If the display is large enough to make the keypad usable even when + rotated, we should not try to rotate it back to the default orientation. + + -- Guido Günther Sun, 15 Nov 2020 11:35:52 +0100 + +phosh (0.5.1) amber-phone; urgency=medium + + [ Guido Günther ] + * debian: Depend on gnome-shell-common. + We use that schema for the keybindings. + * ci: Don't depend on gnome-session. + This would have avoided the missing schema regression + * osk-button: Remove unused defines + * Don't hide OSK on lock screen. + Let is pop up if requested, only hide it when locking. + Closes: #406 + * osk-manager: Fix availability->visibility sync. + Things like osk-button should look unpressed when the dbus name + goes away. + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Марко М. Костић (Marko M. Kostić) ] + * po: Update Serbian translation + + [ Arnaud Ferraris ] + * docked-manager: make tablets dockable too. + The current code considers that only phones are dockable. This patch + improves the device mode check so that tablets also benefit from auto mode + switch (tested on the PineTab). + + -- Guido Günther Tue, 03 Nov 2020 17:18:48 +0100 + +phosh (0.5.0) amber-phone; urgency=medium + + [ Guido Günther ] + * build: Sort enums alphabetically + * enums: Sort types alphabetically + * torch-manager: Cleanup DBus proxy + * feedback-manager: Chain up constructed + * shell: Don't show notification when settings menu is open. + This avoids a notification banner when the notification list + is already open. + * settings: Use g_signal_connect_object. + The panel might go away (e.g. when moved between outputs). This + makes sure the notification list does not send to an already + disposed object. + * settings: Make sure we fill the notification list initially. + Otherwise it remains empty e.g. when we move the primary display. + * lockscreen-manager: Use correct level for debug message + * wayland: Drop unused property setter + * settings: Enforce four quick settings per row. + We allowed to go down to three which makes things look bad when quick + settings information gets too wide (e.g. wifi SSID). + * debian: Support nodoc build profile. + The doc build takes ages and it's often not needed on the device + * monitor: Drop duplicate setting of wl_output_done + * wayland: Drop priv + for less code. And rather check for the right type in public methods + instead. + * osk-button: Show/hide depending on a11y setting. + This makes sure we don't show the button even though + squeekboard would not unfold. (Closes: #363) + * osk-manager: Simplify on_osk_show + * osk-manager: Fix lockscreen_manager typo + * osk-manager: Use g_autoptr + * osk-manager: Only hide the keyboard when we lock the screen. + So far we hid it on every state change. + * osk-manager: Handle visibility property directly. + This makes the visibility property r/o and makes sure + it gets updated when the corresponding DBus property actually changes + (after the async call or when we're told it changed from the OSK). + This makes sure they stay in sync. This is also simpler since we don't + need to bother with variant parsing, etc. + * overview: Focus current activity. + This allows to keyboard navigate on the home screen + * Switch keyboard-events to GAction. + This allows to register global keyboard events from different + parts of the shell easily and moves it's handling out of + the overcrowded settings. + * keyboard-events: Ungrab accelerator when GAction is removed + * overview: Move public functions past `_new` + As per coding style + * app-grid: Move public functions past `_new` + As per conding style. + * overview: Add helper to focus app search + * settings: Ungrab keyboard events (Closes: #398) + * home: Bind key to show overview. + We use org.gnome.shell.keybindings `toggle-overview` and + `toggle-application-view` to focus the application switcher + and app search respectively. + * d/control: Depend on phoc that doesn't crash when unbinding keyboard grabs + * shell: Create toplevel manager early. + So far we were lucky that no wl_display_roundtrip () would let + us miss existing toplevels. + * shell: Process all pending wayland events on startup. + This is needed to get reliable monitor information. + * shell: Pick correct built in monitor. + Simplify the code now that we know we have all the monitor + information (Closes: #392) + * panel: Fix typo + * monitor-manager: Add property for number of monitors. + This allows to listen to a single signal when the number + of monitors changed. + * monitor-manager: move DBus setup into idle callback. + This makes sure we expose the DBus interface past setting + up all the monitor related wayland protocol listeners + * wayland: Add G_{BEGIN,END}_DECLS + * wayland: Provide seat capabilities. + We don't bother to introduce a separate PhoshWlSeat object + for that yet. + * dbus: Add org.freedesktop.hostname1 interface. + So we can cherry the chassis type + * data: Add symbolic phone icons. + From + https://gitlab.gnome.org/Teams/Design/icon-development-kit/ + as of + 0831cbb900e02aaa37bd270a25171c1751394cb8 + * Add mode-manager to handle different device modes. + This uses logind's chassis type and information about + connected hardware to determine the device type and + what the device acts like (e.g. a phone with monitor + and keyboard/mouse should be handled like a desktop). + * Add 'docked' quick setting and manager. + Docked manager figures out if a device has enough + hardware around to be treated as desktop/laptop. + If so it sets the corresponding settings like + floating windows, etc. + The quick setting allows to override this. + * status-bar: Add docked icon when docked. + Show no icon when undocked + * osk-button: Show/hide depending on a11y setting. + This makes sure we don't show the button even though + squeekboard would not unfold. (Closes: #363) + * schema: Add adaptive apps filter entry. + This will allow the shell to have a positive list of adaptive + apps in case their desktop files aren't up to date yet. + + [ Марко М. Костић (Marko M. Kostić) ] + * po: Update Serbian translation + + -- Guido Günther Tue, 27 Oct 2020 10:13:19 +0100 + +phosh (0.4.5) amber-phone; urgency=medium + + [ Anders Jonsson ] + * po: Update Swedish translation + + [ Guido Günther ] + * phosh.desktop: Add 'DesktopNames' + Handle it where it's supposed to be handled. + * shell: Drop env setup. + The session manager is supposed to handle this. + * d/phosh.service: Set XDG_CURRENT_DESKTOP. + Do it here until we run a display manager. + * gitlab-ci: Use libhandy1 for alpine builds. + This fixes the CI builds + * po: Rename pt_PT.po to pt.po. + This allows to fall back for other Portougese translations. + * feedback-manager: Drop TODO. + We use the correct icons since some time + * keyboard-events: Move comment to signal description. + The comment confused gtk-doc since it stared with /** and the signal has + a clearer gtk-doc string already. + * feedback-manager: Fix gtk-doc strings + * home: Fix gtk-doc strings + * home: Properly document enum values. + This allows gtk-doc to pick it up. + * monitor: Make declaration match definition. + We call the object we act on self. + * layersurface: Fix gtk-doc strings. + Document the function parameters + * phosh-wwan-iface: Document interface. + This makes gtk-doc happy. + * phosh-wwan-mm: Mention ModemManager. + Now that we have two implementations mention MM here. + * docs: Add some missing client protocols. + This brings down the number of warnings significantly. + * quick-setting: Fix class names. + Several lacked the Phosh prefix and hence did not turn into links. + * connectivity-info: Fix class name + * bt-info: Fix property separator. + It's ':' for props not '::'. + * shell: Make definition match declaration. + Fixes several gtk-doc warnings + * wwaninfo: Fix class name. + Fixes another gtk-doc warning. + * shell: Add missing parameter docs + * layser-surface: Document configured signal + and fix typos related to that. + * thumbnail: Add class doc. + Avoids another gtk-doc warning. + * panel: Properly document enum. + Use gtk-doc strings to avoid warning and make them show up in + the documentation. + * tools: Add a tool to check for gtk-doc warnings. + This ignores warnings from the generated DBus and wayland + protocols for the moment but reduces the noise so we avoid + errors in new code. + * system-prompter: Unlink PhoshSystemPromtper. + It's not a class, so we can't link to it. + * session: Unlink PhoshSession. + It's not a class, so we can't link to it. Update the doc + string while at it. + * polkit-auth-prompt: Unlink PolkitAgentSession. + Polkitsdocumentation does not end up in /u/s/gtk-doc + on Debian so the reference can't be resolved. + * layersuface: Add missing G_{BEGIN,END}_DECLS + * network-auth-prompt: Add missing G_{BEGIN,END}_DECLS + * docs: Drop gi ref. + We don't use it. + * d/control: Add doc packages. + Needed to get the cross references right + * doc-check: Ignore link to NMClient|NMConnection. + They can' be properly linked to even with the docs installed + due to their odd type. + * monitor: Document PhoshMonitorConnectorType + * notification: Document PhoshNotificationUrgency + * notification: Document PhoshNotificationReason + * shell-network-agent: Stub enum doc. + Use a minimal change since this file is in contrib/. + * gitlab-ci: Run documentation check. + This avoids regression on the generated documentation. We + ignore errors from generated files. + * Rename session presence dbus interface class to PhoshSessionPresenceDBusPresence. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * Rename sensor-proxy dbus interface class to PhoshDBusSensorProxyProxy. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * Rename rfkill dbus interface class to PhoshRfkillDBus. + We have mixed Dbus vs DBus spelling in several interfaces but we want to + use the later. + * docs: Sort in keyboard-events alphabetically + * monitor-manager: Add missing G_{BEGIN,END}_DECLS + * monitor-manager: Use g_autoptr. + Eases upcoming additions + * monitor: Add missing G_{BEGIN,END}_DECLS + * monitor: Make debugging more useful + * shell: Don't allow to set rotation property. + It's marked as read-only + * monitor-manager: Add sections. + This makes it easier to figure out where to add new + private methods. + * Wire up zwlr_output_management_v1. + This does the minimum to maintain a list of heads. A + head represents a display device that might or might + not be part of the current compositor space. + * monitor: Add monitor transforms. + This allows us to not leak the wayland transforms to classes + outside PhoshMonitor and PhoshMonitorManager. + * phosh-head: Add pending state. + This will allow monitor-manager to prepare pending configuration + updates. + * PhoshMonitorManager: Allow to apply state changes. + This adds the methods to change and apply new + configuration state. + * Switch from primary monitor rotation to transform. + Use the output transform in instead of a degree value. + This will allow us to handle flipped configurations correctly. + * shell: Use wlr-output-management instead of phosh private protocol. + This gives us better control which output to rotate and does + away with one more private protocol. + * monitor-manager: Get modes from head instead of monitor + wlr-output-management has all the modes listed while wlroots only + gives us the current mode via xdg_output. + * monitor-manager: Don't apply non persistent config changes. + This makes sure we don't switch the primary display right away + but rather on 'apply'. + * quick-settings: Don't leak panel in error path + * quick-settings: Keep DBus proxy around long enough + g_dbus_proxy_call does not take a ref so we were just lucky + that proxy was still valid in `call_dbus_cb` so far. + * Move helpers to tools/ + There's no clear distinction between them. + * gresources: Sort icons alphabetically + * bt-manager: Fix typo + * settings: Fix indentation error + * settings: Fix misaligned function definitions + * settings: Sort type_ensures and template bindings alphabetically + * settings: Don't let settings_constructed grow out of bounds. + Use separate `setup_` functions for individual widgets. This + helps shuffling stuff around later on and we can drop + create_vol_channel_bar. + * dbus: Add (not yet merged) UPower Torch interface + * Add initial torch support. + Add a simple torch quick setting. + * shell: Fix typo + * shell: Update primary monitor when it goes away (Closes: #382) + * lockscreen-manager: Drop PhoshLockscreenManagerPrivate. + It's a finally type so no need for priv (and it's very unlikely + we'll ever derive here) + * lockscreen-manager: Fix up indentation. + Since we created enough churn in the previous commit let's + fix the indentation too. + * tests: Drop unused test. + It's neither built nor run + * tests: Don't include bad-props.h when unused + * tests: Rework tests using g_test_expect_message. + This no longer works with structured logging so use + g_test_trap_assert_stderr() as recommended by glib docs. + * Use structured logging. + This will pick a suitable log writer automatically and + pass on structured information when e.g. logging to the journal. + * Add a custom log handler. + This is to a large extend what glib does but we don't read the levels + from the environment but allow to set them when setting the handler. + Resetting the handler instead of just adjusting the domains has the + advantage that we can pass in new user_data. Otherwise we'd have to keep + the current log domains around and protect them by an additional mutex + to not corrupt the log domains of handlers running in other threads. + When setting the handler glib handles that for us since handler + replacement is already mutex protected. + * main: Toggle debug messages on SIGUSR1. + We need to use a custom log handler to stay clear + of modifying the env var. + * d/gbp.conf: Automatically bump meson.build version. + No more manual editing. + + [ Juliano de Souza Camargo ] + * po: Update Portuguese translation + + [ Emin Tufan Çetin ] + * po: Update Turkish translation + + [ Julian Sparber ] + * docs: add info about the env GTK_INSPECTOR_DISPLAY + + [ Tobias Bernard ] + * Add torch icons + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + -- Guido Günther Sun, 11 Oct 2020 13:50:48 +0200 + +phosh (0.4.4) amber-phone; urgency=medium + + [ Guido Günther ] + * gitlab-ci: Add i386 cross build. + This ensures we don't break 32bit again. We allow failure + though since we need to pull packages from Debian for that + that might not always be up to date. + * timestamp-label: Unbreak build on 32 bit platforms. + Use a G_ format specifier for GTimeSpan. + * gitlab-ci: Don't use Immediate-Configure. + This confuses apt like. + * media-player: Clarify some translatable strings + * media-player.ui: Match ui strings with the ones in the code + * wwaninfo: Disambiguate 'cellular' + * po: Update pot file + * Drop zanata.xml. + We're using GNOME's translation infrastructure + * README: update description. + * d/rules: Work around failed doc build on install. + This works around a problem where an invocation + of `meson-install` does not generate all docs. See + https://github.com/mesonbuild/meson/issues/2831 + * docs: Split out homepage url + * debian: Build-depend on pandoc for the deb build + * Add gtk-markdown-to-docbook (taken from GTK) + * docs: Add some context + * data/phosh.in: Be a bit more friendly when asked for help + * po: Add initial Korean translation. Thanks Seong-ho Cho + * doap: Add Zander Brown. + * debian: Install all phosh related schema files. + Otherwise we lack the enums and fail to start + * Switch to libhandy 1.0 + * treewide: Replace HdyColumn by HdyClamp + * lockscreen: Adjust to HdyKeypad API changes + * treewide: libhandy API is stable now + * lockscreen: Use HdyCarousel instead of HdyPaginator + * overview: Use HdyCarousel instead of HdyPaginator + * style: Adjust to HdyKeypad internal changes. + The grid is now embedded in the widget. + * gitlab-ci: Use libhandy-1 in smoketests too + + [ Zander Brown ] + * l10n: disambiguate timestamp strings + * po: Update British English translation + * general: update license headers. + Make sure all our license headers match the style in HACKING.md + * ci: validate license headers + * treewide: Use two empty lines between functions. + * arrow: Use G_PI instead of M_PI + * treewide: Don't use C++ style comments. As per coding style. + * general: use char instead of gchar. + Usage of gchar is discouraged in projects such as glib/gtk + In gtk4 all usage of gchar has been dropped + Closes: #364 + * general: use int/double/float instead of gint etc. + Usage of gint,gdouble,gfloat is discouraged in projects such as glib/gtk + In gtk4 all usage of gint/gdouble/gfloat has been dropped + Closes: #364 + * keyboard-events: drop unused props global + + [ anteater ] + * wwan-mm: cleanups for style/consistency + * wwan-mm: use Ugly_Case to correct MMDBus codegen + * wwan: build: reformat + make things more like other meson.build files and avoid rightward drift + * wwaninfo: use PHOSH_WWAN interface to access wwan object + This is more flexible and PhoshWwanInfo doesn't care about the + implementing type + * wwaninfo: reorder includes + * wwan: add PhoshWWanOfono + * shell: add missing whitespace + * shell: clean up dispose, fixing leaks + * shell: reorder includes + * Add and use wwan-backend setting + + [ Luís Fernando Stürmer da Rosa ] + * po: Update Brazilian Portuguese translation + + [ Sebastian Spaeth ] + * po: Fix German translation of Phone Shell. + + [ Nikola Pavlica ] + * po: Add Serbian (Cyrilic) translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Emin Tufan Çetin ] + * po: Update Turkish translation + + [ Fabio Tomat ] + * po: Add friulian translations + + [ scootergrisen ] + * po: Update Danish translation + + [ Марко М. Костић (Marko M. Kostić) ] + * po: Update Serbian translation + + -- Guido Günther Fri, 18 Sep 2020 13:37:36 +0200 + +phosh (0.4.3) amber-phone; urgency=medium + + [ Darren R ] + * Add timestamp-label. The PhoshTimeStamp label display relative times and + will be used in notifications. + + [ chefe ] + * po: Merge all german translations together + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Guido Günther ] + * po: Update pot file + + [ Evangelos Ribeiro Tzaras ] + * Add keyboard forwarding protocol. This will allow the + Volume Buttons to work with recent phoc. + + [ Daniel Mustieles ] + * po: Update Spanish translation + + -- Guido Günther Sun, 02 Aug 2020 18:47:19 +0200 + +phosh (0.4.2) amber-phone; urgency=medium + + [ Guido Günther ] + * shell: Add phosh_shell_get_locked () + This makes it symmetric with `phosh_shell_set_locked ()`. + * settings: Emit feedback when notifications get added. + We only to so when the screen is locked. The feedback is ended + when the list is cleared. + + [ Marc Riera Irigoyen ] + * po: Update Catalan translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Tim Sabsch ] + * po: Update German translation + + [ Daniel Șerbănescu ] + * po: Update Romanian translation + + [ Arnaud Ferraris ] + * d/control: drop Recommends: phoc + phoc is now a dependency, it shouldn't appear in the `Recommends:` + section anymore + + [ Luca Weiss ] + * panel: implement restart action. + Fixes #241 + + -- Guido Günther Fri, 24 Jul 2020 10:55:08 +0200 + +phosh (0.4.1) amber-phone; urgency=medium + + [ Guido Günther ] + * wifimanager: Make variable name match property. Otherwise it's confusing + what it refers too. + * wifimanager: Compare to the old state rather than the probed state. Emit + property changes when the old and new state differ not when our state + differs from detected devices. + * shell: Attach to primary monitors configured signal. This allows us to + emit the 'rotated' signal when it really happened. + * rotateinfo: Mark orientations as translatable + * rotateinfo: Take display geometry into account. + We assumed portrait from the very beginning but that's not correct + on laptops and TVs. (Closes: #326) + * shell: Undo rotation on startup. + This makes sure we get an unrotated shell in any case + (e.g. if phosh crashes in landscape mode) (Closes: #265) + * monitor: Fix several gtk-doc strings. + This fixes some of the warnings during doc generation + * background: Load image async. + This avoids blocking on I/O. + * background: Avoid updating the background twice. + Only update the settings on layer-surface configure but wait for the + monitor::configured signal before drawing anything. + This avoids drawing the background twice on each rotation causing even + more flicker. + * background-manager: Tell background about it's scale + gtk_widget_get_scale_factor gives us the wrong value with multiple + outputs of different scale. + * background: Skip any image loading if layer surface isn't configured. + There's no point to waste resources and we don't have valid dimensios + and it breaks the tests. + * wifiinfo: Sync initial state. + The downside of not using a property binding is that we need to + do that on our own. + * monitor-manager: Set logical position. + This makes the monitor layout in g-c-c match what the compositor thinks + it is so monitors are displayed next to each other rather than over + each other. + * build: Move all dependency() calls to top level build file + * Bluetooth quick setting and status indicator + * background: Avoid warning on NULL pixbuf. + * wwan-mm: Connect to modem async + * wwaninfo: Use operator name as detail. + This gives us the operator name in quick settings + + [ Julian Sparber ] + * AppGrid: Don't set the focus to AppGridButton when clicking on them. + Set the property `focus-on-click` to TRUE so that we don't show the blue + border and don't set the focus to the AppGridButton when clicking on + them. + + [ Emin Tufan Çetin ] + * po: Update Turkish translation + + [ Daniel Șerbănescu ] + * po: Update Romanian translation + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ scootergrisen ] + * po: Update Danish translation + + [ Balázs Meskó ] + * po: Update Hungarian translation + + [ Sebastian Krzyszkowiak ] + * overview: Clean up unneeded size checks. + + -- Guido Günther Mon, 06 Jul 2020 10:25:20 +0200 + +phosh (0.4.0) amber-phone; urgency=high + + [ Guido Günther ] + * feedbackinfo: Add translation note. + Link to feedbackd's theme description to help translators + * media-player: Don't act on every name change. + We were looking at all vanished DBus names instead of only players. This + lead to a slight flicker of the widget e.g. on audio events or + notifications (which was especially noticeable on the lock screen). + * PhoshLayerSurface: Add missing sentinel + * Add small testlib allowing for wayland based tests using phosh-wayland + and spawning a phoc instance. Add two initial tests using that. + * monitor: Properly invaliate wl_output_done. + On `wl_output` configuration changes invalidate the done flag + so another `configured` signal is emitted once all changes + are processed. So far we only handled the initial configure. + * phosh-wayland: Clarify ownership of phosh_wayland_get_wl_outputs + * shell: Do nothing when primary monitor does not change + * shell: Don't clear faders when we dispose the panels + * status-icon: Drop show-always (Closes: #312) + * Add WWAN quicksetting + * gitlab-ci: be more robust when using xvfb + * background-manager: Avoid recreating backgrounds too often + Only do so initially and let PhoshBackground adjust to configuration + changes notified by PhoshBackground instead. + * background: Skip image loading when using colored mode. + There's no need to even try to load an image if we're not using + it later on. + * tests: Add a simple background test. + * d/rules: Depend on phoc >= 0.4.0. + While we can run with older versions this makes sure people + pull in a recent enough version for all the feaures. + + [ Daniel Șerbănescu ] + * po: Update Romanian Translation + + [ Sebastian Krzyszkowiak ] + * PhoshHome: Use PhoshLayerSurface signals and props to handle resizing. + This fixes the issue with disappearing bottom bar when output dimensions + change and it actually more in line with what we wanted to achieve there. + * PhoshWayland: Don't require phosh_private interface in latest version. + We can still deal with earlier ones. + * Render window thumbnails in the overview + * wwaninfo: Use network-cellular-disabled-symbolic when no hw is present + * background: Stop listening to `rotation` + PhoshMonitor now signals rotation correctly + + [ marty1885 ] + * monitor: prevent sending multiple event in one monitor configuration + change + * background: recreate background on monitor geometry change + + [ Julian Sparber ] + * wifimanager: Use disabled icon for unknown states. + * Add Wifi quicksetting + + -- Guido Günther Tue, 30 Jun 2020 14:14:27 +0200 + +phosh (0.3.1) amber-phone; urgency=medium + + [ Sebastian Krzyszkowiak ] + * PhoshOverview: Add haptic feedback when clicking on activity + * feedback-manager: Add a helper to trigger feedback on button press/release + + [ Daniel Șerbănescu ] + * po: Add Romanian translation + + [ Rafael Fontenelle ] + * Update Brazilian Portuguese translation + + [ Guido Günther ] + * feedback-manager: Make 'connect_feedback' specific to buttons. + Those have 'clicked' and 'released' signals. + * app-grid-button: Make sure we unref the allocated gesture + * build: Bump version + * lockscreen: Drop HDY_DISABLE_DEPRECATION_WARNINGS. + We're not using any deprecated widgets + * lockscreen: Add G_{BEGIN,END}_DECLS + * Lots of gtk-doc fixups + * Build documentation via gtk-doc. + We need a shared lib for gtk-doc, only build this + when building Documentation. + Closes: #88 + * debian: Add phoc-doc. + This makes the documentation usable via devhelp. + * gitlab-ci: Build documentation + * docs: Document C file layout + * gitlabci: Move coverage gathering to different step + Fixes spurious build failures. + * phoc.ini: Drop cursor config. + Phoc does not care anymore and with one output it does not match + anyway. So people with multiple outputs and touch need to run + the latest phoc release. This avoids a warning on phoc startup. + * phosh: Indicate connectivity state. + We show no icon if we're connected but indicate the lack of a connection + (or if we have a connection and can't reach the internet) + Closes: #229 + * brightness: Connect async to g-s-d. + Shell startup sometimes stalls for a longer period of time. Connect + async to g-s-d to avoid at least one stall spotted with gdb. + It also fixes a leaked DBus connection. + * css cleanups + * tests: Don't try to spawn atk bridge. + * Add media-player widget. + This interfaces with mpris Mediaplayer2 based players + Closes: #94 + * settings: Add media player. + Will only be shown when a player is present + * lockscreen: Add media player widget + * po: Update pot file. + This introduces the new translatable strings for the music player. + * layer-surface: Add the configured width and height to debug message. + We have that anyway so make it more useful + * layer-surface: Fix indentation + * layer-surface: Add a section header + * top-panel: Drop useless placeholder + * top-panel: Remove extra vertical padding from power button + * gitlab-ci: Skip po and doc when using PKG_ONLY + * media-player: Wire up Can{Go{Forward,Previous},Play} + This makes sure we don't enable ui elements the player can't handle + * media-player: Add a playable property. + The player might be stopped or not have a song list. Add a playable + property so the container has a better idea if it's worth showing + the player. + * lockscreen: Use playable property of media player. + We don't need a widget for a stopped or unusable player. + * settings: Use playable property of media player. + We don't need a widget for a stopped or unusable player. + Closes: #315 + * feedbackinfo: Listen for feedback changes. + When switching from silent to quiet (and back) the icon name does not + change so we need to listen to profile name changes to update the + text below the icon. + + [ Zander Brown ] + * app-list-model: phantom launcher. + At last a fix for the missing item problem + + [ Yuri Chornoivan ] + * po: Update Ukrainian translation + + [ Alexander Mittermeier ] + * Remove reference to rootston as the link was broken and the project is + deprecated. https://github.com/swaywm/rootston + + -- Guido Günther Mon, 22 Jun 2020 16:27:59 +0200 + +phosh (0.3.0) amber-phone; urgency=medium + + [ Arnaud Ferraris ] + * monitor: add "power-mode" property. + In order to be able to notify other objects when the power state of a + monitor changes, this commit adds a `power-mode` property to the monitor + class. + * shell: lock screen when builtin monitor is powered off + * src: fix typos + * phosh.service: don't hardcode user name. + In order to allow the user to change the default username, use the UID + instead of the username. + * po: fix French translation. + This patch fixes the long date format on the lockscreen. + + [ Mohammed Sadiq ] + * polkit-auth-prompt: Use symbolic icon + + [ Guido Günther ] + * settings: Drop PhoshSettingsPrivate. + We'll never derive from that widget so drop the indirections. + * settings: Ensure types during class_init. + No need to do so during each instance creation. + * panel: Drop unused height getter + * shell: Move settings into top bar widget. + This will allow us to drop the xdg popup code. + (Closes: #155, #216) + * shell: Drop xdg popup handling. + We don't use it atm and if we need popups again we want to wrap + them in proper objects. + * panel: Set keyboard interactivity. + This allows us for keyboard navigation in the settings menu + * panel: Disallow focus on panel widgets. + We only want to keyboard navigate settings + * settings: Fix QuickSetting keyboard focus behaviour. + We set each QuickSetting to not-focusable since we otherwise + have to press TAB twice on each button: once for the flowbox + focus and one for the button focus. + To trigger the QuickSetting we wire up the FlowBox's `child-activated` + signal. + * panel: Close settings menu on ESC and when clicking/touching into empty + area. + * smoketest: Keep phoc around. + Phoc changed to exit after the session exited but the smoke tests rely on + a running compositor so keep it around to parse some parameters. + * po: Last time sync from zanata. + We're switching to GNOME infra. + * README: Point to GNOMEs translation system + * panel: Drop superfluous gettext define. + We have gi18n.h for that. + * monitor: Allow to set power save mode. + This allows to set a monitors power save mode: + OFF: monitor is turned off + ON: monitor is turned on + We don't use the constants from the wayland protocol + to not leak the implementation. + * Process enums for monitor.h too. + They will be used in the following commit + * shell: Add method to turn on power saving. + This method can be used to turn power saving on or off. + Currently it only turn on power saving for the display but + this can be extended in the future. + * monitor: Use PhoshMonitorPowerSaveMode enum. + This makes sure we treat the wayland protocol enum as internal + to the monitor implementation. + * monitor-manager: Remove unused PowerSaveMode + * panel: Add a GtkStack to switch top bar. + The top bar has a different content when the settings menu is + unfolded. Add a stack for that. + * panel: Move shutdown to power popover + * Move power off from settings menu to top bar + * Move lock screen button to power popover + * session: Add logout. + Since we don't have a logout dialog yet we don't ask gnome-session + to show one either. + * panel: Wire up logout (Closes: #234) + * settings: Drop button to launch settings. + This frees up space for the notification area. + * panel: Keep settings widget around. + This avoids e.g. a jumpy volume slider when unfolding the + settings menu. We manage it outside the stack for later gesture support. + * wifimanager: Avoid excessive g_strdup() on signal strength changes. + Suggested by Zander Brown. + * wwaninfo: Avoid excessive g_strdup() on signal strength changes + * status-icon: Don't leak icon name + phosh_status_icon_get_icon_name() returns a copy so we need to + free it and also can't compare by address. + * tests: Test phosh_status_icon_set_icon_name() + * gitlab-ci: Barf on files with translations but not in POTFILES.in + * po: Add files not needing translations to POTFILES.skip + * po: Sort POTFILES.in alphabetically + * po: Add missing source file (Closes: #305) + * po: Update pot file + * gitlab-ci: Allow to skip build and test. + Sometimes we just want a package (e.g. when only changing the packaging or + when we're sure(tm) we didn't break anything). Hence allow to skip the + build and test stages via `PKG_ONLY = 1` + * data: Rename compositor config to phoc.ini. + * data: phoc.ini: Don't fall back to rootston. + This gives confusing error messages when e.g. phoc is not installed. + * data: phoc.ini: Honor WLR_BACKENDS. + * settings: Hide notification box when empty + * notification-tray: Move style closer to designs. + Use a gray background for the tray and no drop shadows for individual + bubbles. + * settings: Close settings after acting on the last notification + * feedback-manager: Add helper to trigger async feedback. + We don't pass the manager itself since it's not needed. + * home: Emit feedback event on home bar press + * overview: Trigger feedback on window close + * panel: Trigger feedback when top bar is clicked. + This can go away once we have gestures in place. + * app-grid: Provide haptic feedback on app launch + * shell: Avoid signal emission when shell is already locked. + LockscreenManager got this right but the shell would emit a signal on + every invcation although the state would not change. + * MonitorManager: Use wlr-power-manager to set handle GNOMEs power_save_mode + requests. This allows the screen to blank when g-s-d thinks so. + * shell: powersave: Just lock the screen. + With the monitor-manager's PowerSaveMode wired up it's enough to lock + the screen to make g-s-d then tell us to blank the screen. + This makes sure compositor, shell and g-s-d have the same idea about + the screen state. + + [ Efstathios Iosifidis ] + * Update Greek translation + + [ Rafael Fontenelle ] + * phosh.doap: fix URL to source code repository + * Update Brazilian Portuguese translation + + [ Danial Behzadi ] + * Add Persian Translation + * Add Persian to Linguas + + [ Zander Brown ] + * utils: phosh_clear_handler, g_clear_handler for older GLib. + This can go away once we dep on 2.62 + * notification-frame: fix a leak when header tapped + get_item returns a reference which we weren't dropping causing some + notifications to live forever + * notification: store the urgency/transient/resident/category hints + urgency/transient/resident modify the behaviour of the message tray + category is included for completeness, we may never use it + (sound hints are still ignored as we don't declare support for it) + * notifications: a model for notifications from a single source + * notifications: keep a list of notifications + * tests: test the new notification models + * notifications: maintain a message list + expired notifications are no longer automatically dismissed + * notifications: add list to settings drop-down + * tools: explain notify-[blocks/server-standalone] use + + [ Yuri Chornoivan ] + * Update Ukrainian translation + + [ Yi-Jyun Pan ] + * i18n: zh_TW: update translation + https://l10n.gnome.org/vertimus/phosh/master/po/zh_TW/ + + -- Guido Günther Tue, 19 May 2020 08:08:34 +0200 + +phosh (0.2.2) amber-phone; urgency=medium + + [ Guido Günther ] + * wwan: use interface prefix. This shortens the function names + * WWan: Connect to ModemManager async. + The rest is all property access which is async. + (Closes: #289) + * wwan: Free GDBusObjects too. We only freed the list itself so far. + * lockscreen: Drop unused variables + * settings-menu: Drop expand from feedback button. + This makes sure we don't use up extra space when other elements get + hidden (like the music player) + * shell: Avoid gtk_widget_show_all. + This is needed in preparation for #155 anyway. + * lockscreen: Fix unlock page type. + It's a grid not a box + * network-auth-prompt: Inform user about unknown auth types. + This avoids another empty dialog similar to #292. + + [ Arnaud Ferraris ] + * wifimanager: create network-auth-prompt only for WiFi connections. + While the network-auth-prompt only supports only WiFi connections, it + would still show (empty) if NM sends a secrets request for WWAN. + This patch makes sure the prompt isn't created if the connection type + isn't WiFi. (Closes: #292) + + -- Guido Günther Fri, 03 Apr 2020 11:26:01 +0200 + +phosh (0.2.1) amber-phone; urgency=medium + + [ Guido Günther ] + * feedbackinfo: Use 'On' for the 'full' feedback theme. + See: #276 + * wwaninfo: Don't overwrite sim missing status. + This allows the sim-missing status to be displayed. (Closes: #281) + * monitor: Wire up zwlr_output_power_management. + * protocol: Generate protocol with private code. + That's sufficient since we don't want to export anything + * po: Update translations from zanata + Thanks to all the translators! + * Settings: wire up feedback setting long press. + This opens the notifications panel now that we can configure + feedback there. + * Shell: Don't access variable before type check + * Feedbackinfo: Toggle to silent instead of quiet. + Also turn of vibra with the quick setting. Vibra can be turned on via + g-c-c. + * gitlab-ci: Run xvfb-run with no-reset. + This avoids spurious test failures. + + [ Rasmus Thomsen ] + * gitlab-ci: pull in feedbackd on Alpine too + fixes #278 + + [ Julian Sparber ] + * Wifimangager: set icon-name always via the same function. + + [ Zander Brown ] + * protocols: generate with public-code not code + public-code does the same thing but without logging warnings + * app-list: fix lookup cache. + We never invalidated the cache leaving us in an invalid state after + install/remove + This potentially explains the missing icons that turn up after reboot + (probably) fix https://source.puri.sm/Librem5/phosh/issues/213 + * apps: use a common placeholder icon (Closes: #197) + + [ Darren R ] + * README: Explain how to skip gnome-session setup. + This avoids spawning unintended services. + + [ Sebastian Krzyszkowiak ] + * panel: Clean up unused widget references + + -- Guido Günther Thu, 19 Mar 2020 19:07:18 +0100 + +phosh (0.2.0) amber-phone; urgency=medium + + [ Guido Günther ] + * Upload to amber-phone (since amber has no feedbackd yet) + * phosh: Register the session late so calls sees the right status + Closes: https://source.puri.sm/Librem5/calls/issues/126 + * Proximity sensor support + Closes: #120 + * Add Feedback quick settings to toggle current feedback status. + * wifimanager: consistently update icon on enable/disable/kill switch + * Fix build with meson 0.53 + * Fix build with tests disabled + * build: Allow to build libfeedback as subproject. + * phosh: Use drm backend by default to help wlroots 0.10.0 and hence phoc + 0.1.6. + * Update translations including 7 completely new ones. Thanks everyone! + + [ Sebastian Krzyszkowiak ] + * ui: Adjust the icon margin of backlight slider in settings menu. + + [ Julian Sparber ] + * Improve lockscreen unlock button styling + * Introduce QuickSetting base class for quicksettings + * Introduce StatusIcon base class for top bar icons and + quicksettings + * Groundwork for WWan and Wifi quicksettings + * Add BatteryInfo quick setting and show percentage + Closes: #268 + * Make rotation a quick setting + + [ Zander Brown ] + * Groundwork for future notification improvements: + - split out notification content from banner + - split the frame from the banner. + - track notifications not widgets as preparation + - introduce NotificationFrame + * editorconfig: strip whitespace. + + -- Guido Günther Wed, 26 Feb 2020 09:34:47 +0100 + +phosh (0.1.8) amber; urgency=medium + + [ Julian Sparber ] + * Lockscreen: Replace submit btn with unlock btn + * Fader: fade completely to black on shutdown. + Closes: #257 + * Fix brightness slider being jumpy while sliding. + Closes: #258 + * StatusIcon: Introduce base class for status icons + and use for top panel icons. + + [ Guido Günther ] + * phosh-system-prompt: Toggle ok button sensitivity. + Pressing so far caused to send e.g. an empty pin to a smart card + and we'd rather not run out of retries so require a non empty password. + * build: Bump version to 0.1.8 + * build: Use libhandy 0.x branch if used as a subproject. + Closes: #264 + + [ Zander Brown ] + * build: bump glib dep. + We where using stuff from 2.54 already and buster ships 2.58 + * notifications: separate the data and widget. + Move notification information into a separate class from the banner, + moving forward this will ease lockscreen/notification drawer + implementatinon + + -- Guido Günther Sat, 08 Feb 2020 13:44:20 +0100 + +phosh (0.1.7) amber; urgency=medium + + [ Darren R ] + * Update meson.build to show version 0.1.6 + + [ Guido Günther ] + * phosh.in: Use builtin session handling. + Until we can rely on a newer gnome-session use the builtin session + handling everywhere for now instead of systemd. (Closes: #181) + * gitlab-ci: Build an arm64 deb + * wifi-manager: Rework connection handling. + So far we gave up early when the currently active connection did not + have any devices. It seems this can happen early up in the connection + process of *new* connections. Hence always redo the device check on + connection state changes. + * wifimanager: Consolidate device cleanup. + * wifimanager: Clean up device state when (de)activating a connection. + Don't use the device and access point information before we're + actually associated. Otherwise we show strength information from + outdated access points. (Closes: #202) + * lockscreen-manager: Undo rotation when locking the screen. + This makes sure we don't leave the user with a screen that can't be + unlocked. (Closes: #73) + * auth: Drop pin length limitation. (Closes: #233, #234) + * network-auth-prompt: Allow to toggle password visibility. + This makes entering wifi passwords a bit simpler. Inspired by + GTK4's GtkPasswordEntry. + + [ Julian Sparber ] + * Lockscreen: add submit button, allows arbitrary pin lengths. + This also adds the emergency button but for now it is hidden, because + emergency calls don't work yet. + + -- Guido Günther Thu, 09 Jan 2020 15:34:36 +0100 + +phosh (0.1.6) amber; urgency=medium + + [ Julian Sparber ] + * Lockscreen: replace HdyDialer with HdyKeypad. + The new Keypad uses a GtkEntry to store the entered pin/password. + * Lockscreen: use GcrSecureEntryBuffer for the pin entry. + * BatteryInfo: drop private data for gobject. + Since the BatteryInfo is a final it can't be subclassed, therefore it + doesn't need private data. + + [ Guido Günther ] + * background: Plug leak + * home: Plug leak + * topevel: Avoid NULL check + * toplevel: Don't leak app_id and title + * toplevel: Guard handle from being destroyed multiple times + * wwaninfo: Separate access tec and signal srength (Closes: #118) + * top-panel: Adjust label margins. + Use the same margins as for other text in the top panel. + * top-panel: Use only right margins. + + [ Darren R ] + * lockscreen: Use show_symbols False + + -- Guido Günther Fri, 27 Dec 2019 23:05:07 +0100 + +phosh (0.1.5) amber; urgency=medium + + [ Guido Günther ] + * rootston.ini: Drop keyboard config section. + Not parsed anymore by phoc, uses GSettings now + * Turn on autorestart. + Mitigates #212 + * gitlab-ci: Switch to shared gitlab-ci jobs. + This gives us more tests with less code. + * debian: Add trivial autopkgtest. + This makes sure we at least install correctly + * wifimanager: Don't leak icon name + * d/control: Depend on the used gsettings schema + * gitlab-ci: Test pureos against staging. + * build: Make libhandy dep explicit + * wwan-mm: Give up if we can't get proxy. + Just printing a warning is no enough. + * wwaninfo: Only show wwan icon if a modem is present + (Closes: #64) + + [ Julian Sparber ] + * Bump libhandy to 0.0.12 + + [ Alexander Mikhaylenko ] + * lockscreen: Allow mouse dragging for lockscreen. + See https://source.puri.sm/Librem5/libhandy/merge_requests/344 + + -- Guido Günther Fri, 13 Dec 2019 17:43:41 +0100 + +phosh (0.1.4) amber; urgency=medium + + * activity: Add title setter + * activity: Set app_name on title changes. + So far we only did this in the constructor. + * toplevel: Emit signal when properties change. + So far we emitted signals when a new toplevel shows up. Add + a signal that is emitted when a already 'configured' toplevel + gets configured event. This will allow to e.g. check for + title changes. + * overview: Adjust title in overview when the toplevel changes title. + We take a detour via the overview since the link between + activity and topleve happens there. We can avoid this + once the activity properly owns the toplevel. + * tests: Avoid duplicate config target + otherwise we fail with meson 0.49. + * gitlab-ci: Build pureos amber package as well + * panel: Move decision if we need a lang label into one place. + This makes sure we do the same on input source changes and keyboard + connect/disconnect. + * debian/gbp.conf: Sign tags by default + + -- Guido Günther Fri, 22 Nov 2019 17:16:45 +0100 + +phosh (0.1.3) amber; urgency=medium + + [ Guido Günther ] + * main: Simplify signal handling + * main: Handle SIGINT as well. + * main: Don't initially lock screen when we were started by gdm + * main: Add -L to force a locked shell. + * brightness: Fix brightness setting. + * top-panel: Display current keyboard layout if a keyboard is connected. + This makes tings less conusing when using/testing different layouts + The layout is only displayed when a keyboard is connected to the + seat. + * session: Unset DESKTOP_AUTOSTART_ID. + This makes sure we don't leak it to child processes. + + [ Sebastian Krzyszkowiak ] + * activity: Handle apps with "gnome-" prefix in their app ID. + Otherwise we end up with "org.gnome.Gnome-software" kind of names. + * util: Move app_id fix up code to separate function + * toplevel: Rename phosh_toplevel_raise to _activate for consistency + * app-grid-button: Activate existing window instead of trying to relaunch. + Fixes #102 + + [ Zander Brown ] + * build: only rebuild generated sources when linking tests/tools. + This avoids extra (wasted) build steps for tools/tests/etc + * app-button: don't emit app-launched when an error is set. + Previously we emitted even when GLib reports an error + * app-button: store favourites in a model, add action menu. + * tests: improve testing of app-button. + * tests: add tests for the favourites model. + * favourites: use en_US spelling. + + -- Guido Günther Fri, 15 Nov 2019 01:54:40 +0100 + +phosh (0.1.2) amber; urgency=medium + + [ Bart Ribbers ] + * gitlab-ci: don't build test coverage for Alpine Linux + + [ Guido Günther ] + * Add wayland session file. + This allows to run phosh directly from e.g. gdm which makes testing + on GNOME way easier + * Recommend phoc. We don't make it a hard dependency since we can run with + other compositors as well. + * Drop all rootston fallbacks. rootston is unmaintained and has only very + limited functionality while phoc had a release. + * debian: Bump compat to 12. + This makes some things simpler like dh_nissing and it moves + phosh to /u/libexec where it should have always been. + * debian: Add install file for phosh. + This makes us pick up the right files when we add more binary packages + * build: move glib/gio dependency to toplevel meson file. + This and assigining it to variables makes it simpler for + other meson files to use. + * Add keyboard stub. + This helps to fulfil gnome-session dependencies on desktops where we + don't have squeekboard (yet) and where it should not unfold + * layer-surface: Emit signals on width/height changes. + We missed signals on phosh_layer_surface_set_size(). + * layer-surface: Don't make width/height construct only. + * po: Update translatable files + * po: Update translations from zanata + + [ Dorota Czaplejewicz ] + * layersurface: remove phosh-wayland include. + This makes the file copy-portable again. + * layer-surface: Distinguish requested and configured width/height + {width,height} represent the user request while + configured_{width,height} represent what the compositor sent us. + This allows clients to see how their size request matches the + actual surface size. + + [ Mohammed Sadiq ] + * Add shell-network-agent. + * Add network agent. Network agent is responsible for requesting credentials + when connecting to networks that requires credentials + + -- Guido Günther Fri, 25 Oct 2019 21:07:24 +0200 + +phosh (0.1.1) amber-phone; urgency=medium + + [ Guido Günther ] + * lockscreen: Use proper ellipsis. + Use '…' instead of '...' + * session: Drop org.gnome.SettingsDaemon.Clipboard. + It was unused anyway. (Closes: #195) + * notify-manager: ignore errors closing inexistent notifications. + This trips up qt 5.11 otherwise + * debian: Don't restart session on package upgrades. + Otherwise a logged in user will get logged out. (Closes: #193) + * gitlab-ci: Make current image explicit + * gitlab-ci: Build and unit test for Debian Bullseye as well. + We keep the smoketests at buster for the moment + * debian: Add gbp.conf. + This makes `gbp tag` do the right thing. + * Update translations from zanata + + [ Hysterical Raisins ] + * AppGrid: Live update search when using an input method. + + [ Bart Ribbers ] + * gitlab-ci: Build and unit test for Alpine Linux edge as well + + -- Guido Günther Fri, 11 Oct 2019 11:54:52 +0200 + +phosh (0.1.0) amber-phone; urgency=medium + + [ Zander Brown ] + * app-grid: align top + * overview: focus styling, follow focus + * app-grid: enter activates first app search result. + Fix https://source.puri.sm/Librem5/phosh/issues/171 + * app-grid: hint app that will launch on enter + * notifications: support images, bring styles close to design + * notifications: add actions. + This means firefox doesn't complain anymore and actions are clickable + * notifications: handle display scaling and rotation + * notifications: slide down from top + * tools: test notification sender + * notifications: emit notify::app-info + * apps: display name not display name. + This makes sure apps have the same name as in shell + + [ Alexander Mikhaylenko ] + * overview: Remove close on click. + It's triggered where swiping PhoshPaginator. I'm not sure this is wanted + from design standpoint anyway, so remove it for now. + * overview: Use paginator for switching apps + + [ Guido Günther ] + * polkit-auth-agent: Allow to cancel dialog via escape key. + I've seen gnome-shell users hitting that a lot. + * system-prompt: Allow to cancel dialog via escape key. + I've seen gnome-shell users hitting that a lot. + * notify-manager: Handle org.gnome.desktop.notifications show-banners. + This allows to globally enable / disable message bunners aka + 'notification popups' as gnome-control-center calls them. (Closes: #167) + * build: sort source alphabetically + * session: Reindent. + Reindent according to coding style + * Add fader that fades from transparent to almost black. + We might want to make this more flexible later so we can also use + it for the fade in animation on startup. + * session: Use fader on shutdown. + This gives immediate visual feedback. + * main: Fade out on sigterm + * po: Update translatable files + * session: Only show fader on EndSession. + QueryEndSession only asks for a shutdown, we don't want to fade + in that case. + * Kill fader after 15 seconds + if we can't shut down promptly kill the fader instead of leaving + the shell in an unusable state. + * auth: Unconditionally set pamh to NULL. + Don't do it only in the successful case since the handle is + not useful afterwards and later calls might otherwise reuse the handle. + Also don't use the handle in the error path after pam_end. + * rootston.ini: Remove 'binding' section. + This is now handled via gsettings and triggers a warning + on phoc startup. + * po: Update translatable files + * po: Update translations from zanata + + [ Julian Sparber ] + * Lockscreen: Give visual feedback on wrong password. + This makes the dots shake when the user enters a wrong password + * Lockscreen: add hack to remove # and * form the keypad + * Lockscreen: Style keypad buttons. + Fixes: phoc#18 + * AppGrid: make search case insensitive. + Fixes: #186 + * AppGrid: Make line below favorites darker and thinner + fixes #182 + * AppGrid: use a revealer to hide the favorites when search is started + fixes: #185 + * AppGrid: increase spacing around line below favorites + + [ Sebastian Krzyszkowiak ] + * PhoshBackgroundManager: Divide the background size by scale. + Without this the background surface buffer is needlessly oversized, + putting unnecessary load on the GPU. + * toplevel: Add "activated" property and its getter + * overview: Scroll to currently focused activity + * style: Remove unnecessary blending. + GTK uses software rendering and pixman isn't optimized on aarch64. + This makes blending huge surfaces pretty expensive - and there's + no reason to do that if we're blending black on black anyway. + This makes fullscreen scrolling in app drawer way smoother. + + [ Hysterical Raisins ] + * AppGrid: hide favorites on search + - conserve vertical screen space (which will be at a premium when both osk + and overview are visible at the same time) + - blend in favorites when searching + Fixes phosh#177 + * NotifyManager: plug a leak in variant iteration + * AppGrid: Don't activate first item unless search has focus. + Fixes #177 (for real this time) + * AppGrid: Correct return types for focus events + * AppGrid: plug casefold leaks. + Fixes #190 + + [ Tobias Bernard ] + * Style: prettier search bar + - Minor styling changes (rounded corners, more padding) + - Add placeholder text + * Style: adjust margins around search bar and favorites. + Fixes an issue with icons being cut-off only on the sides + when scrolling. + * Minor: fix ellipsis and string in search placeholder + + -- Guido Günther Mon, 30 Sep 2019 10:54:01 +0200 + +phosh (0.0.4) purple; urgency=medium + + [ Guido Günther ] + * PhoshLayerShell: Don't realize layer shell's right away. + This allows us to create them early and keep them around. It also + makes them behave more like regular Gtk widgets. (Closes: #91) + * system-prompt: Use a template instead of builder. + This makes this prompt consistent with all the other layer surfaces we + have. + * debian: We can act as polkit auth agent + * debian: Update description a bit + * po: Update LINGUAS + * system-prompt: Wrap on word boundary. + Otherwise we overflow on small screen sizes + * system-prompt: Add a border. + This makes sure we don't have text and buttons pressed to the left and + right screen edges. + * lockscreen: Add date (Closes: #101) + * Update pot file with new strings. + The date format needs a string to be printed correctly in the + different locales. + * lockscreen: Move date, time and icons into a common box. + This allows spacing. + * po: Update translations from zanata + * favorites: Drop labels. + This makes the "overview issue" go away with up to 8 favorites. + This improves the status quo until we fix #89 and merged !187. + * favorites: Trim down app list. + These are the ones that the focus is on atm. (Closes: #100) + * css: Drop HdyArrow css. + We're currently not using this widget + * css: Make polkit auth agent's background black too + * SystemPrompt: Set grid alignment in the ui file + * system-prompt: Right align labels. + Following https://developer.gnome.org/hig/stable/visual-layout.html.en + * system-prompt: Move checkbox below text inputs + * system-prompt: Adjust spacing. + Following https://developer.gnome.org/hig/stable/visual-layout.html.en + * polkit-auth-prompt: Right align labels. + Following https://developer.gnome.org/hig/stable/visual-layout.html.en + * polkit-auth-prompt: Adjust spacing. + Following https://developer.gnome.org/hig/stable/visual-layout.html.en + * polkit-auth-prompt: Use bold font for user name + * polkit-auth-prompt: Remove empty row + * PolkitAuthAgent: Remove duplicate gtk_widget_show () + * PolkitAuthPrompt: Translate most common request + * po: Update translatable files + * po: Update pot file + * shell: Activate background again + https://github.com/swaywm/wlroots/issues/897 is true independent if + there's background surface or not so we're not making matters worse + by enabling it. (Closes: #76) + * PhoshPanel: Make signal handlers consistent + * PhoshLockscreen: include locale.h. + Fixes build with -O0 + * PoshBackground: Drop things now handled by PhoshLayerSurface + * PhoshBackground: Use g_autoptr instead of explicit unref + * PhoshBackground: Get width and height from layer surface + * PhoshBackground: Drop pointless defaults from init + * PhoshBackground: Validate signal handler signatures + while at that move 'self' first like in the rest of the codebase. + * PhoshBackground: unref settings + * lockscreen: Set wifi icon visible by default (Closes: #111) + * PhoshBackground: Drop priv. + Less code and consistent with other new code. + * PhoshBackground: Listen to configured signal. + This is simpler then g_signal_override_class_handler() + * lockscreen: Determine wifi status on idle. + This makes sure we determine the wifi status after the widget set up. + Otherwise the widget might sometimes stay invisible. + * PhoshShell: Make background surface span the whole output. + The background will make sure to not draw over/under panels. (Closes: #6) + * PhoshBackground: Implement zoom style. + Listen for picture-option and handle scaled (as already implemented) + and zoom. If the style is unknown default to zoom like gnome-shell. + Code taken from GnomeBG. + * PhoshBackgroundManager: Add manager to handle multiple outputs. + This handles multiple output making sure we update the backgrounds + on output changes. (Closes: #113) + * PhoshBackground: Use g_object_connect + * PhoshBackground: Use defines for setting keys. + We need them in several places. + * PhoshBackground: Don't hardode grey as color. + This allows to set the background color from gsettings. + * PhoshFavorites: Scale icon images to 64px + * favorites: Don't scale by output scale. + Otherwise icons are way to large. + * favorites: Use gtk_image_set_pixel_size. + This leaves the scaling to gtk. + * PhoshWWanInfo: Set pixel_size when setting widget size + * PhoshWWanInfo: Don't round trip through pixbuf when using theme icons. + There's no need to roundtrip through a pixbuf when we're using theme + icons. This removes blur from the no-sim and sim locked case. + See: #118 + * favorites: Drop custom drawing + * service: Restart on compositor crashes. + So far we wanted to keep the state around for debuggig but it's stable + enough now that we can restart. + * layersurface: Use phosh_wayland_get_default() everywhere. + This makes it consistent with _mapped() + * Add editorconfig. + Prompted by tabs in ui files. + * PolkitAuthPrompt: Make background app paintable. + This makes it consistent with the gcr system modal dialogs. + * Use the same style class for gcr and polkit prompts. + This makes sure they stay visually consistent. + * PhoshSystemPrompt: Swap ok and cancel. + Conform to GNOME HIG. + * PolkitAuthPrompt: Swap ok and cancel. + Conform to GNOME HIG. + * app: Add close button. + Emit a 'app-closed' signal when that button is clicked. + * Update phosh private protocol to v3 + * PhoshFavorites: Close application on 'app-close' signal (Closes: #67) + * tests: Use phoc instead of rootston. + This is our primary compositor and while the usage of the phosh-private + protocol is optional if we bind to it we require at least version 3 + which is not supported by our rootston fork. + * PhoshLayerSurface: Destroy layer surface on unmap. + This and moving some of the initialization from `realized` to `mapped` + allows us to properly `gtk_widget_{show,hide}()` layer surface widgets. + This is brought over from squeekboard + c3a54595ea72638fbc7b65ffc3c26966de772e15 + * PhoshHome: Drop priv. + This makes the code smaller and easier to read. + * build: Fix indentation. + We want two spaces. + * Use glib-mkenums. + This will allow us to use enums as types. + * PhoshLayerSurface: Allow to set size of the layer surface. + This can be useful to fold/unfold surfaces. + * PhoshHome: Clarify PHOSH_HOME_HEIGHT. + Rename to PHOSH_HOME_BUTTON_HEIGHT since to not confuse the home button + with the home screen (which takes the full screen except for the panel). + * PhoshHome: Track folded/unfolded state. + We fold/unfold the surface on home button clicks. + We use an enum since we might end up with half folded states + or a different state when using app switch via swipe. + This surface will later on have the app drawer. (Closes: #89) + * PhoshHome: Show favorites on home screen. + Instead of making PhoshFavorites a separate layer surface popup show the + widget on the home screen by deriving from GtkBox instead of GtkWindow. + * PhoshActivity: Rename activity-closed signal. + The name is misleading since the signal is emitted when the close + button is clicked not when the actual activity is being closed. + Rename to 'close-clicked' to make that obvious. + * favorites: Replace assert by g_return_if_fail () + * favorites: Use g_return_if_fail in type check. + This makes sure we don't silently ignore the error. + * PhoshTopLevelManager: Move function assignments to top. + This makes it consistent with other classes. + * PhoshTopLevelManager: Move public methods to the bottom. + This makes it consistent with other classes. + * PhoshToplevelManager: Add num-toplevels property. + This allows us to keep track about opened toplevels. + * PhoshShell: Unfold home screen when no application is running. + When we don't have any wayland toplevels show the overview. (Closes: #99) + * PhoshActivity: Remove unused defaults + * PhoshAcivities: Drop superfluous braces + * PhoshActivities: Match property defaults to what's set in `_init()` + * PhoshActivities: Mark properties as G_PARAM_EXPLICIT_NOTIFY. + Because they are. + * PhoshAcitivity: Avoid superfluous object emission and resizes. + No need to do anything if the property does not change. + * PhoshLayerSurface: Be verbose when acking configure. + This is an important event when surface properties change. + * Add gtk4's list model. + This is from commit ddc74a08bea170c73effe6eddca45a077bf130a7 + * PhoshHome; Make Image a child of the button. + This allows for more fine grained styling. + * PhoshHome: Use CSS animations to change home button arrow direction + * PhoshHome: Set initial arrow state to 'up' + Initially the surface is folded. That doesn't matter when starting up + but it matters when phosh restarts with windows already open. + * PhoshFavorites: Rename to create_favorite. + We create a widget but don't add it to the flowbox yet. + * PhoshFavorites: Use g_auto() + * PhoshFavorites: Show created favorite. + This makes sure changing favorites refreshes without restarting phosh. + This wasn't a problem when favorites was a pop up since the widget + got recreated when opened. + Brokey-by: bc2198f6bbe8d3132d711cfa30719578eada5473 + * PhoshFavorites: Avoid gtk_widget_show_all() + All widgets are being gtk_widget_show()n correctly now. + * home: Drop superfluous gtk_widget_{show,hide} + * Settings: Add type checks to callbacks + while debugging it became obvious that we don't type check + here which makes life cycle issue fixing harder then necessary. + * Settings: Swap signal emission and lock trigger. + The lock trigger brings up new surfaces which discards the popup so the + object might not exist anymore once we want to emit the signal. + Fixes + (phosh:5485): GLib-GObject-WARNING **: 17:11:59.262: instance with invalid (NULL) class pointer + (phosh:5485): GLib-GObject-CRITICAL **: 17:11:59.262: g_signal_emit_valist: assertion 'G_TYPE_CHECK_INSTANCE (instance)' failed + Broken-by: cb7b20ebf94177199d6a7347d5fe48b144bf81f0 + * Settings: rotation_changed_cb: Move self in front. + This makes it match the other call backs. + * LockscreenManager: Only lock session when we go idle. + So far we locked on any status change which didn't matter since we + only get the idle information. Now that we get org.gnome.ScreenSaver + we get other states reported as well. + * LockScreen: Emit signal when the output should be woken up. + This is useful to indicate that the output used by the lock screen + should be woken up e.g. on keypress. + * LockScreenManager: Remember activation time. + Save the point in time the lock screen gets activated, this will + be used by the screen saver manager. + * LockscreenManager: Emit signal when all screens should be woken up. + This is mostly meant for org.gnome.ScreenSaver + * Add PhoshScreenSaverManager. + This adds the skeletion for the org.gnome.ScreenSaver + DBus API that allows applications get the lock state + and ask for screen lock. + * ScreensaverManager: Hook up to LockscreenManager. + Implement org.gnome.ScreenSaver by invoking the appropriate methods + on our Lockscreenmanager and listen to it's relevant signals. + We keep this in a separate object to keep the lock screen code + separate from the DBus interaction. + We inject the lock screen manager instead of fetching the singleton + to make the code more self contained. + * helpers: Add script to check screen saver state + * Lockshield: Use type as parent, not class + * LockShield: Drop unneeded custom rendering. + We don't need a drawing area and no custom rendering when we just + want to render a black background. + * tests: Test AppGridButton + * AppGridButton: Remove superfluous initializers + * AppGridButton: Rename pspec to props. + Just cosmetics to make it consistent with the other classes. + * AppGridButton: Whitespace cleanups + * AppGridButton: Use getters and setters for GAppInfo. + This makes the property handler sway smaller + * PhoshAppInfo: Add tests for new methods and constructor + * gitlab-ci: Add coverage information + * README: coverage badge. + This is a bit misleading since it only covers unit test coverage and + even that doesn't run all the tests. + * tests: Add app-list-model tests + * tools/app-scroll: Fix activity scaling. + Otherwise activities are just as large as the close button. + * tools/app-scroll: Drop max-* properties. + They were dropped from app-grid-button as well. + Spotted-by: Zander Brown + * Overview: Rename overview evbox and flowbox to apps. + This contains the application (favorites and later on installed + applications). + * lockscreen: Slide lockscreen down on timeout. + This makes the lock screen slide down instead of coming back up after + timeout. (Closes: #105) + * layer-surface: Allow to set keyboard interactivity + * home: Move fold/unfold logic into a single if/else + * home: Allow keyboard input when unfolded. + This allows application search to receive keyboard input and will + e.g. allow keyboard based window switching later on. + * defaults: Drop terminal from favorites. + With the app drawer it's easily accessible and we're back + to one line of favorites which looks way better. + * Update translations from zanata + * build: bump version. + We released 0.0.3 a while ago + * Add PhoshNotifyManager. + This will handle the org.freedestkop.Notifications DBus protocol + * phosh-notification: Add notification widget. + This is a layer surface that handles displaying a single notification. + When adding notifications to the lock screen, etc. we will likel make + this a data only type that is consumed by the various implementations + but that depends on how the designs for this will look like. + * notify-manager: Create basic notifications. + Activities, animating slide in/out, showing notification on the lock + screen, do not disturb mode and silencing individual application are + left as follow-ups. + * debian: Provide notification-daemon. + We provide the DBus daemon and basic functionality + * Bump libhandy requirement. + The arrows need 0.0.11 + * notifications: Don't leak app_icon + * Release phosh 0.0.4 + + [ Tobias Bernard ] + * Top panel: move clock down a tiny bit + * Lockscreen: improve spacing on clock, date, icons + * Minor: lockscreen.ui indentation + * Lockscreen: Stop arrow animation after 15 cycles. + This should mitigate #108 somewhat. + * Settings: make circular button styling specific to settings + * Favorites: remove button styling and circular shape, add icon shadow. + This also makes 4 icons fit into onto the screen at 360px. + * Favorites: highlight on click, instead of hover + * App switcher: No margin and border radius on the title overlay + * Fake app placeholder: round top corners + * App Switcher: styling for temporary close button + + [ Dorota Czaplejewicz ] + * session: Use the default OSK instead of Virtboard + + [ Sebastian Krzyszkowiak ] + * PolkitAuthAgent: fix build with Polkit >= 0.114. + Polkit 0.114 introduced bunch of predefined g_autoptr cleanups, which + leads to compilation failure due to duplicate cleanup definition for + PolkitSubject. Note that even the latest polkit doesn't have a cleanup + for PolkitAgentListener defined, so it's left there unguarded. + * PhoshBackground: Draw the wallpaper at full resolution on HiDPI screens + * background: Unescape the URI of the background file. + Fixes #130 + * Use wlr-foreign-toplevel-management instead of private protocol. + Introduces PhoshToplevelManager and PhoshToplevel classes for + managing and representing toplevel surfaces. + Thanks to that: + - it doesn't match the window to close via its title or app id, + always trying to close the correct window instead + - it removes the window from the overview only after it really got + closed - which is especially noticeable in case of windows that + refuse to be closed + - it updates the list automatically as the changes happen, without + having to close and open the overview back again + Closes #37 + * PhoshApp: Rename to PhoshActivity. + Single application can be represented by multiple PhoshApps. It's also + not a window, as in the future, multiple windows may be represented + as a single PhoshApp as well - therefore, PhoshActivity seems to be + the most fitting name to this entity. + * PhoshHome: Hide the keyboard button when unfolded + * PhoshHome: Don't set the width of the home bar. + When setting 0 as width value, the compositor will set the width + automatically according to anchors. + This fixes an issue of folded home bar size not being updated after screen + rotation until it's unfolded. + * PhoshLayerSurface: Allow to set anchor margins of the surface + * PhoshLayerSurface: Allow to change the exclusive zone value + * PhoshHome: (Un)folding animation. + Uses layer-surface margins to animate the unfolding without resizing + the surface. + * PhoshHome: Adjust arrow transition duration to match unfolding time + * PhoshHome: Show correct arrow orientation with GTK animations disabled + * shell: Fold home screen when a new toplevel appears. + Closes #154 + * home: Fold on Escape key press. + This doesn't matter much for phone, but makes the interaction much + more pleasant on desktop. + * overview: Hide the list of activities when there are none. + This allows the app launcher to take all available space. + * phosh-wayland: Don't require wlr-foreign-toplevel-manager extension. + It's not very widely implemented yet and it's considered a privileged + protocol that's unlikely to be exposed by default anyway. + Closes #144 + * layersurface: Add phosh_layer_surface_wl_surface_commit function + * home: Remove gtk_widget_queue_draw from (un)folding animation. + This was causing a massive performance drop on the devkit, while a surface + commit is all that's really needed there. + * home: Reset the app grid state when unfolding. + This includes clearing the search field and scrolling the list to the top. + * shell: Fold the home screen when unfolding settings screen + * shell: Hide the keyboard when unfolding the home screen + * app-grid: Use g_auto cleanup macros + * app-grid: Store the list of favorites. + This will allow to skip allocate/free cycle per every entry when filtering. + * app-grid: Don't show favorites on the app list. + Fixes #164 + + [ Zander Brown ] + * build: use the kwarg for PIE + * switcher: Rework app switcher. + This moves the window title and application icons to the bottom of the + running application widget and uses a fake window where the actual app + content will go later on. + * build: handy as a subproject. + Some of us don't want to install handy master globally + * general: ignore VSCode settings/metadata + * build: split some components into static lib + makes tests/demos simpler + * tools: app-scroll demo. + Simple demo of PhoshApp + * tests: use the full name not just the first letter. + Meson being too clever for it's own good + * favorites: Switch to kgx (Closes: #133) + * gtk-list-model: Adjust to build out of gtk4's tree + * Add PhoshAppGridButton. + This will be used in the app drawer for launchers + * Favorites: Use PhoshAppGridButton. + This will be used in the app drawer for all launchers but for now it's + just favourites + * launcher: force wrapping of long titles + * app-grid-button: handle NULL app-info + * launcher: import app-list-model. + Taken from my maynard fork + * overview: rename PhoshFavorites now it's function has changed. + The overview is the widget that contains the + - activities (running applications) + - the favorites + - the app drawer + * app-grid: AppGrid widget to launch installed applications + * overview: Add app-grid + * tools: Add wrapper to run standalone tools. + Similar to phosh's run to set e.g. the schema dir + * tools: Add tool to show app-grid outside the shell. + Provider wrapper to setup env to run tools + * overview: Move favorites launchers into app-grid. + They inetract closely, e.g. on search so it makes sense to have them in + the same widget. + TODO: the css name change from phosh-favorites to phosh-overview should + go in a separate commit (Closes: #38) + + [ Adrien Plazas ] + * system-prompt: Style overhaul. + This makes system prompts look more like part of the shell than part of + apps. + + [ Sybolt de Boer ] + * Use g_value_set_int() to set an int + + [ Heather Ellsworth ] + * Add Contacts to list of favorites + + [ Alexander Mikhaylenko ] + * favorites: Resize apps before size allocation. + Instead of connecting to 'size-allocate' signal on evbox_running_apps, + override size_allocate vfunc on the window itself, then do the resizing + before chaining up. + This will allow to avoid glitches with paginator. + * activity: Simplify sizing. + Follow window aspect ratio without minimum width and height. Switch to + width-for-height geometry management. + When favorites widget is allocated, set window width and height from + favorites's own allocation before chaining up, this avoids any glitches + on the first allocation. + Remove now unused max-width and max-height properties. + * Introduce PhoshArrow. + An animated arrow. This will be used for home/overview transition. + * home: Use PhoshArrow for animation + * home: Use libhandy animation functions. + See https://source.puri.sm/Librem5/libhandy/merge_requests/308 + * meson: Bump libhandy version to 0.0.11 + * lockscreen: Fix indentation in xml + * lockscreen: Hide dialer action buttons in xml. + There's no point in doing this only on swipe up. + * lockscreen: Follow finger when swiping. + Pack the two pages into a PhoshPaginator. This also allows to swipe back + from keypad to the info screen. + Remove the GtkEventBox around the info page, as the paginator is already + a subclass of GtkEventBox. + + [ Hysterical Raisins ] + * LayerSurface: constructed() needs to be chained up + + -- Guido Günther Sat, 07 Sep 2019 01:36:19 -0700 + +phosh (0.0.3) purple; urgency=medium + + [ Guido Günther ] + * IdleManager: Use G_GUINT64_FORMAT. + We missed that one. This breaks the build on armhf. + * Monitor: Save transform too + * Monitor: Allow to detect flipped outputs + * Monitor: Allow to get rotation by degrees + * Shell: Determine usable area by output transform. + Instead of looking at our rotation property look at the monitors actual + transform instead. (Closes: #55) + * MonitorMnager: Report transform too + * MonitorManager: Fix formatting + * Shell: Fix type of 'rotation' property + * Shell: Derive rotation property from primary monitor's rotation. + This way they can never get out of sync. + * Shell: Setup a primary monitor after probing outputs. + This way we don't have to make any assumptions. We don't do this via + set_primary_monitor since we don't want to trigger panel recreation to + avoid flickering. + * Settings: Switch shutdown and lockscreen. + This matche gnome-shell's layout. + * Settings: Make sure the buttons stay circular + * MonitorManager: Specify integer types in g_variant_builder_add. + This makes sure we treat guint64 correctly. + * App: Don't print title if it's identical to the applications name. + This avoids printing the same thing twice. + * rooston.ini: Drop scale=2. + We had this for the testing the OLED displays. The devkit has a separate + HDMI to attach large screens. + * gitignore tag files + * Fix URLs and copyright holder. + We moved repos a while ago + * rootston.ini: Scale rootston on DSI-1 output. + The devkit's LCD panel is DSI-1 and we want scaled output there. + * session: Remove incorrect error parameter in DBus call + * run: Allow to skip gnome-session. + It's not always needed and floods the CI logs + * ci: Skip gnome-session for valgrind run. + This makes the logs better readable and we don't need it there. + * meson: Print phosh version as well + * run: Use glib's valgrind suppressions as well + * settings: Add separate meson file. + It's better to track sources per directory. + * Settings: Simplify rotate evaluation + * Add gvc submodule. + This submodule will be used for volume control. It's also used + by gnome-shell and g-c-c. + * Wire up output volume control. + Based on gvc-channel-bar from gnome-control-center. We'll split this out + of settings.c once we handle more volume controls. + * ci: Drop valgrind run until we have more RAM. + It seems the build node drops out atm + * settings: Make settings use the full screen width. + Wrap it in a HdyColumn for that to not look totally out of place + on large screens. + * Remove some unused components from the session. + This brings down the number of X11 dependencies and drops things that + crash frequently (although unused). We can add these back later once + needed and when the necessary wayland protocols are in place. + * Favorites: Use a GtkBox instead of a GtkFloxBox for running apps. + This allows to scroll vertically + * rootston.ini: Map seat to DSI-1 instead of HDMI. + This makes rotation work and it's far more likely to be used on the LCD + atm. This needs a proper fix in the compositor to be better at input + mapping (https://source.puri.sm/Librem5/wlroots/issues/23). + * gitlab-ci: Build debs too + * shell: Prefer dark theme. + See: https://source.puri.sm/Librem5/librem5-base/issues/6 + * PhoshWayland: Add and use phosh_wayland_roundtrip. + Based on a patch by Simon Ser. + * PhoshLockscreenManager: Add missing chain up in constructed() + * PhoshLockscreenManager: Drop unused parent + * PhoshWwanInfo: Fix callback signature. + We don't use the other params but it's better to have this correct. + * PhoshWwanInfo: Simplify signal handler disconnects + * PhoshPanel: Fix element order. + This makes glade happy so we get no diff when saving. + * top-panel.ui: Fix indentation + * PhoshShell: Delay gui setup. + If we do it in an idle callback we're sure the shell object is already + there so we have all the managers at our disposal to e.g. connect + widgets to them. + * PhoshShell: Make custom widget types known. + This makes sure we can use them in UI files independent from any loading + order. + * Add PhoshWifiManager. + This keeps track of the wifi devices by interfacing with NetworkManager + and gets the current signal ruality. We delay it's creation until it's + needed. + We can later on add support for ad-hoc networks. + * Add PhoshWifiInfo widget. + This connects to the wifi manager to display the current wifi state. + Using it in the panel is then just a matter of adding it to the ui file. + * PhoshLockScreen: Display wifi status. + Hook the wifi status into the lockscreen too + Use the WifiInfo widget here too. (Closes: #10) + * d/control: Build-dep on at-spi2-core. + On the devkit we don't pull in recommends but GTK+ wants it to run the + tests in a meaningful way. + * Add HACKING to clarify coding style and other matters + * README: Add pipeline status + * README: Make sure build-deps are up to date. + Use the tested list from d/control instead of hardcoding it. + * PhoshMonitor: Emit signal when monitor is fully configured. + Emit a signal when we received all of the configuration data from + the compositor. + * PhoshMonitor: Update comment + * PhoshMonitorManager: Fix function declaration. + Use a better variable name. + * PhoshWayland: Maintain outputs in hash table. + This duplicates the static wl_ouput GPtrArray which will be removed + at the end of this series. + * PhoshWayland: listen for outputs going away + * PhoshWayland: Add functions to query outputs + * PhoshMonitorManager: Pick up changed wl_outputs + and signal when we added/removed a monitor. (Closes: #46) + * Move initial handling from PhoshShell to PhoshMonitorManager. + This move wl_output processing into the one place that has a way better + idea what to do. + * PhoshWayland: Kill the GPtrArray in favour of the GHashTable + * LockScreenManager: Listen for monitor changes (Closes: #51) + * PhoshLockscreenManager: simplify signal disconnects + * Use #pragma in all headers + * Add PhoshOskManager. + Class to handle OSK interaction. + * PhoshShell: Add PhoshOskManager + * PhoshOskButton: Use PhoshOskManager. + Don't duplicate code that is in PhoshOskManager and keep the DBus + interaction in one place. + * PhoshOskButton: Drop private data + * PhoshShell: Add getter for lockscreen manager + * PhoshOskManager: Hide keyboard on screen lock (Closes: #75) + * PhoshShell: Close OSK when displaying favorites or settings (Closes: #81) + * README: Prefer wlroot's X11 backend when nested. + This one gets the window size right. + * rooston.ini: Use the phone's resolution with the X11 backend + * PhoshLockScreen: Only show clock on the lock screen. + We don't want date or weekday there. (Closes: #85) + * helpers: Make it simple to use glade with PhoshLayerShells. + Temporarily replace the PhoshLayerShell by a GtkWindow known to glade. + We could extend the glade-catalog but this way doesn't require any + special setup. + * rootston.ini: Don't scale X11 display. + This helps people with screens that don't have 1440 vertical pixels. + * phosh.in: Prefer phoc over rootston + * PhoshLayerSurface: Don't use gtk_widget_show_all() + gtk_widget_show ought to be enough. Widgets need to setup themselves + properly. + * Become a policy kit authentiation agent. + Somewhat based on gnome-shell's implementation. (Closes: #22) + * debian: Add dependency so we can be a polkit auth agent + * pot: Update translatable strings + * Update translations from zanata + * rootston.ini: Fix horizontal resolution. + We have 360 not only 320 pixels. + Thanks to Zander Brown + * Lockshield: Use black background too + * Lockscreen: Make arrows a bit thinner + * Home: Make center button expand again. + This unbreaks clicking the home bar. Otherwise only the image + itself would be sensitive. + * css: Add unit to font size. + Silences a gtk warning. + * PhoshWWanInfo: Drop priv. + We're not going to derive from it and this makes the code simpler. + * PhoshWWanInfo: Allow to set icon size + * rooston.ini: Set window size for wayland as well + * README: Fix some wording + * README: Don't special case running nested on x11 + wayland works too. + + [ Dorota Czaplejewicz ] + * build: Use the right path to the source. + Tests wouldn't build if the build directory was outside of the source + tree. + * build: Remove duplicated gio dependency + + [ emersion ] + * Update the wlr-layer-shell-unstable-v1 protocol + * PhoshWayland: Add wlr_output_manager v1 protocol. + We don't make the lack of that protocol fatal atm since the rootston + currently on the devkit doesn't have that code yet. + + [ Zander Brown ] + * Make top and home-bar black. + + [ Tobias Bernard ] + * Top panel: equal spacing on icons on left and right + * Top panel: make time bold + * Settings menu: Black background, simplified styling + * Lock screen: black background, margin fixes, thinner clock font + * Minor: Change "PIN" to "Passcode" + * Home bar: use custom, wider swipe arrow icon + * Home bar: move keyboard button to the right + * Home bar: rounded, smaller OSK button + * Lockscreen: replace HdyArrows with a an image + CSS animation + * Settings menu: add button outlines, improve spacing + * Minor: better top bar clock style class name + * Top bar: make status icons 16px + + -- Guido Günther Wed, 19 Jun 2019 09:07:57 +0200 + +phosh (0.0.2) purple; urgency=medium + + [ Guido Günther ] + * Update translations from zanata + * home: Add keyboard button. + Add a keyboard button to the home buttons bar. This will allow to unfold + the OSK. + * monitor-manager: Use output names. + This allows us to give the monitors more meaningful names instead of + made up numbers and to correctly identify built in displays. + * shell: Move panels on primary monitor change. + When a new primary monitor is set move the panels around. + This also fixes the home panel not being disposed. + * system-prompter: Pass on the wl_output and not the monitor to new + prompters. This makes sure they always end up on the primary monitor. + * Add basic org.gnome.Mutter.Idle DBus protocol support. + This allows e.g. g-s-d power to track our idle time for e.g. dimming the + display. + * LockscreenManager: Rely on gnome-session for idle detection. + This makes us properly respect idle inhibitors e.g. when watching movies. + * Favorites: Drop weston terminal. + Don't hardcode weston terminal anymore since gnome-terminal now also on + the dev boards. + * Settings menu: Add shutdown button + + [ David Boddie ] + * Updated Debian package dependencies and .ini file location. + + -- Guido Günther Sat, 01 Dec 2018 17:59:07 +0100 + +phosh (0.0.1) unstable; urgency=medium + + * Initial release + + -- Guido Günther Wed, 31 Jan 2018 15:02:24 +0100 diff --git a/debian/clean b/debian/clean new file mode 100644 index 000000000..ad0c83de5 --- /dev/null +++ b/debian/clean @@ -0,0 +1,4 @@ +debian/phosh.postinst +debian/phosh.postrm +debian/phosh.service +debian/phosh.triggers diff --git a/debian/control b/debian/control new file mode 100644 index 000000000..b9154644a --- /dev/null +++ b/debian/control @@ -0,0 +1,247 @@ +Source: phosh +Section: x11 +Priority: optional +Maintainer: Guido Günther +Build-Depends: + debhelper-compat (= 13), + gi-docgen , + gsettings-desktop-schemas-dev (>= 47), + libadwaita-1-dev, + libappstream-dev, + libcallaudio-dev, + libevince-dev, + libgirepository1.0-dev, + libgmobile-dev, + libjson-glib-dev, + libsecret-1-dev, + libsystemd-dev, + libfeedback-dev (>= 0.7.0), + libfribidi-dev, + libgcr-3-dev, + libgirepository1.0-dev , + libgnome-bluetooth-3.0-dev, + libglib2.0-dev (>= 2.72.0), + libgnome-desktop-3-dev, + libgtk-3-dev, + libgtk-4-dev, + libgudev-1.0-dev, + libhandy-1-dev (>= 1.1.90), + libmm-glib-dev (>= 1.24.0), + libnm-dev, + libpam0g-dev, + libpolkit-agent-1-dev, + libpulse-dev, + libupower-glib-dev, + libwayland-dev, + libxml2-utils, + meson, + python3-docutils , + valac, +# for the plugins + libecal2.0-dev, + libqrcodegen-dev, + evolution-data-server-dev, + universal-ctags, +# to run the tests + at-spi2-core , + black , + desktop-file-utils , + flake8 , + dbus-daemon , + gnome-settings-daemon-common , + gnome-shell-common , + gnome-themes-extra-data , + gsettings-desktop-schemas , + gtk-4-examples , + librsvg2-common , + network-manager , + phoc (>= 0.44~) , + python3-dbusmock (>= 0.32.1) , + python3-pytest , + gir1.2-umockdev-1.0 , + umockdev , + wlr-randr , + xauth , + xmlstarlet , + xvfb , +Standards-Version: 4.7.0 +Homepage: https://phosh.mobi/ +Rules-Requires-Root: no + +Package: phosh +Architecture: any +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + fonts-lato, + gnome-shell-common (>= 49), + gsettings-desktop-schemas (>= 49), + libcap2-bin, + libqrcodegen1, + phoc (>= 0.52.0), + phosh-common (= ${source:Version}), +Recommends: + feedbackd (>= 0.5.0), + fonts-adwaita, + gnome-session-bin (>= 49), + gnome-session-common, + gnome-settings-daemon (>= 49), + iio-sensor-proxy, + librsvg2-common, + network-manager-config-connectivity-debian, + kbd, + phosh-mobile-tweaks, + phosh-plugins, + slurp, + phosh-osk-stevia, +Provides: + notification-daemon, + polkit-1-auth-agent, +Breaks: + gnome-control-center (<< 42), + gnome-settings-daemon (<< 49), + libgtk-3-0 (<< 3.24.30), + xdg-desktop-portal-phosh (<< 0.44~), +Description: Pure Wayland shell for mobile devices + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + It's part of the Phosh Mobile Environment based on GNOME/GTK. For the full + stack see the phosh-full and phosh-core metapackages. + +Package: phosh-doc +Architecture: all +Multi-Arch: foreign +Section: doc +Build-Profiles: +Depends: + ${misc:Depends}, +Description: Pure Wayland shell for mobile devices - development documentation + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains the development documentation. + +Package: phosh-common +Architecture: all +Multi-Arch: foreign +Depends: + ${misc:Depends}, +Conflicts: phosh (<< 0.45~rc1-3~) +Description: Pure Wayland shell for mobile devices - Common files + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains arch-independent files needed by phosh and libphosh. + +Package: phosh-mobile-tweaks +Architecture: all +Depends: + ${misc:Depends}, + dconf-gsettings-backend | gsettings-backend, +Description: Pure Wayland shell for mobile devices - GSettings tweaks + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains settings to improve behaviour on mobile devices. + +Package: phosh-plugins +Architecture: any +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + gsettings-desktop-schemas, + phoc (>= 0.13.1), + phosh (= ${binary:Version}), +Breaks: + phosh-mobile-settings (<< 0.44~), +Description: Pure Wayland shell for mobile devices - Plugins + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains additional plugins. + +Package: phosh-dev +Architecture: any +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + gsettings-desktop-schemas-dev, +Description: Pure Wayland shell for mobile devices - development files + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains the development files. + +Package: libphosh-0.45-dev +Architecture: any +Section: libdevel +Build-Profiles: +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + libappstream-dev, + libcallaudio-dev, + libfeedback-dev, + libgcr-3-dev, + libgmobile-dev, + libgnome-bluetooth-3.0-dev, + libgnome-desktop-3-dev, + libgudev-1.0-dev, + libhandy-1-dev, + libmm-glib-dev, + libnm-dev, + libphosh-0.45-0 (= ${binary:Version}), + libpolkit-agent-1-dev, + libpulse-dev, + libsecret-1-dev, + libsoup-3.0-dev, + libupower-glib-dev, + phosh-dev (= ${binary:Version}), +Breaks: + libphosh0-42-dev, +Replaces: + libphosh0-42-dev, +Description: Pure Wayland shell for mobile devices - binding development files + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains the development files for language bindings + +Package: libphosh-0.45-0 +Architecture: any +Section: libs +Multi-Arch: same +Build-Profiles: +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + phosh-common (>= 0.45.0), +Description: Pure Wayland shell for mobile devices - binding shared library + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains the shared library for language bindings. + +Package: gir1.2-phosh-0-dev +Architecture: any +Section: libdevel +Build-Profiles: +Depends: + ${misc:Depends}, + ${shlibs:Depends}, + libphosh-0.45-0 (= ${binary:Version}), +Description: Pure Wayland shell for mobile devices - GObject introspection data + Phosh is a graphical shell for Wayland compositors speaking the layer-surface + protocol and aimed at mobile devices like smart phones and tablets using touch + based inputs and small screens. + . + This package contains the GObject introspection data. diff --git a/debian/copyright b/debian/copyright new file mode 100644 index 000000000..1c90365e6 --- /dev/null +++ b/debian/copyright @@ -0,0 +1,593 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: phosh +Upstream-Contact: Guido Günther +Source: https://gitlab.gnome.org/World/Phosh/phosh + +Files: * +Copyright: 2000 Eazel, Inc. + 2007-2008 Red Hat, Inc. + 2014 Collabora Ltd + 2018-2020 Purism SPC + 2019-2020 Zander Brown + 2019 Alexander Mikhaylenko +License: GPL-3+ + +Files: debian/* +Copyright: 2018 Purism SPC + 2020 Arnaud Ferraris +License: GPL-3+ + +Files: protocol/wlr-foreign-toplevel-management-unstable-v1.xml +Copyright: 2018 Ilia Bozhinov +License: MIT + +Files: protocol/wlr-output-management-unstable-v1.xml + protocol/wlr-output-power-management-unstable-v1.xml +Copyright: 2019 Purism SPC +License: MIT + +Files: src/contrib/* +Copyright: 2011 Red Hat, Inc. + 2011 Giovanni Campagna + 2017 Lubomir Rintel +License: GPL-2+ + +Files: src/gtk-list-models/gtkfilterlistmodel.* + src/gtk-list-models/gtksortlistmodel.* +Copyright: 2018 Benjamin Otte +License: LGPL-2.1+ + +Files: src/gtk-list-models/gtkrbtree* +Copyright: 2000 Red Hat, Inc., Jonathan Blandford +License: LGPL-2+ + +Files: subprojects/gvc/* +Copyright: 2006-2008 Lennart Poettering + 2008-2009 Red Hat, Inc. + 2008 William Jon McCann + 2008 Sjoerd Simons + 2009 Bastien Nocera + 2011-2012 Conor Curran + 2012 David Henningsson, Canonical Ltd. +License: GPL-2+ + +Files: + subprojects/libcall-ui/* +Copyright: + 2019-2022 Purism SPC +License: LGPL-2.1+ + +Files: + subprojects/libcall-ui/src/cui-dialpad.* +Copyright: + 2018-2022 Purism SPC + 2021 Thomas Booker +License: LGPL-2.1+ + +Files: + subprojects/libcall-ui/examples/images/cat.jpg +Copyright: UNKNOWN +License: CC0-1.0 +Comment: + From gnome-control-center, + see https://bugzilla.gnome.org/show_bug.cgi?id=792243 + +Files: + subprojects/libcall-ui/examples/icons/* + subprojects/libcall-ui/src/icons/* +Copyright: + 2019 Purism SPC + 2019 GNOME Design Team +License: CC-BY-SA-4.0 + +License: GPL-3+ + This package 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 3 of the License, or + (at your option) any later version. + . + This package 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, see + . + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". + +License: LGPL-2+ + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + . + This library 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 + Library General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + +License: LGPL-2.1+ + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 2.1 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 Lesser General Public License for more details. + . + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . + +License: MIT + Permission is hereby granted, free of charge, to any person obtaining a copy of + this software and associated documentation files (the "Software"), to deal in + the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do + so, subject to the following conditions: + . + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + . + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. + +License: GPL-2+ + This package 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 package 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, see + . + On Debian systems, the complete text of the GNU General + Public License version 2 can be found in "/usr/share/common-licenses/GPL-2". + +License: CC0-1.0 + On Debian systems the full text of the CC0-1.0 license can be found in + /usr/share/common-licenses/CC0-1.0 + +License: CC-BY-SA-4.0 + Attribution-ShareAlike 4.0 International + . + ======================================================================= + . + Creative Commons Corporation ("Creative Commons") is not a law firm and + does not provide legal services or legal advice. Distribution of + Creative Commons public licenses does not create a lawyer-client or + other relationship. Creative Commons makes its licenses and related + information available on an "as-is" basis. Creative Commons gives no + warranties regarding its licenses, any material licensed under their + terms and conditions, or any related information. Creative Commons + disclaims all liability for damages resulting from their use to the + fullest extent possible. + . + Using Creative Commons Public Licenses + . + Creative Commons public licenses provide a standard set of terms and + conditions that creators and other rights holders may use to share + original works of authorship and other material subject to copyright + and certain other rights specified in the public license below. The + following considerations are for informational purposes only, are not + exhaustive, and do not form part of our licenses. + . + Considerations for licensors: Our public licenses are + intended for use by those authorized to give the public + permission to use material in ways otherwise restricted by + copyright and certain other rights. Our licenses are + irrevocable. Licensors should read and understand the terms + and conditions of the license they choose before applying it. + Licensors should also secure all rights necessary before + applying our licenses so that the public can reuse the + material as expected. Licensors should clearly mark any + material not subject to the license. This includes other CC- + licensed material, or material used under an exception or + limitation to copyright. More considerations for licensors: + wiki.creativecommons.org/Considerations_for_licensors + . + Considerations for the public: By using one of our public + licenses, a licensor grants the public permission to use the + licensed material under specified terms and conditions. If + the licensor's permission is not necessary for any reason--for + example, because of any applicable exception or limitation to + copyright--then that use is not regulated by the license. Our + licenses grant only permissions under copyright and certain + other rights that a licensor has authority to grant. Use of + the licensed material may still be restricted for other + reasons, including because others have copyright or other + rights in the material. A licensor may make special requests, + such as asking that all changes be marked or described. + Although not required by our licenses, you are encouraged to + respect those requests where reasonable. More_considerations + for the public: + wiki.creativecommons.org/Considerations_for_licensees + . + ======================================================================= + . + Creative Commons Attribution-ShareAlike 4.0 International Public + License + . + By exercising the Licensed Rights (defined below), You accept and agree + to be bound by the terms and conditions of this Creative Commons + Attribution-ShareAlike 4.0 International Public License ("Public + License"). To the extent this Public License may be interpreted as a + contract, You are granted the Licensed Rights in consideration of Your + acceptance of these terms and conditions, and the Licensor grants You + such rights in consideration of benefits the Licensor receives from + making the Licensed Material available under these terms and + conditions. + . + . + Section 1 -- Definitions. + . + a. Adapted Material means material subject to Copyright and Similar + Rights that is derived from or based upon the Licensed Material + and in which the Licensed Material is translated, altered, + arranged, transformed, or otherwise modified in a manner requiring + permission under the Copyright and Similar Rights held by the + Licensor. For purposes of this Public License, where the Licensed + Material is a musical work, performance, or sound recording, + Adapted Material is always produced where the Licensed Material is + synched in timed relation with a moving image. + . + b. Adapter's License means the license You apply to Your Copyright + and Similar Rights in Your contributions to Adapted Material in + accordance with the terms and conditions of this Public License. + . + c. BY-SA Compatible License means a license listed at + creativecommons.org/compatiblelicenses, approved by Creative + Commons as essentially the equivalent of this Public License. + . + d. Copyright and Similar Rights means copyright and/or similar rights + closely related to copyright including, without limitation, + performance, broadcast, sound recording, and Sui Generis Database + Rights, without regard to how the rights are labeled or + categorized. For purposes of this Public License, the rights + specified in Section 2(b)(1)-(2) are not Copyright and Similar + Rights. + . + e. Effective Technological Measures means those measures that, in the + absence of proper authority, may not be circumvented under laws + fulfilling obligations under Article 11 of the WIPO Copyright + Treaty adopted on December 20, 1996, and/or similar international + agreements. + . + f. Exceptions and Limitations means fair use, fair dealing, and/or + any other exception or limitation to Copyright and Similar Rights + that applies to Your use of the Licensed Material. + . + g. License Elements means the license attributes listed in the name + of a Creative Commons Public License. The License Elements of this + Public License are Attribution and ShareAlike. + . + h. Licensed Material means the artistic or literary work, database, + or other material to which the Licensor applied this Public + License. + . + i. Licensed Rights means the rights granted to You subject to the + terms and conditions of this Public License, which are limited to + all Copyright and Similar Rights that apply to Your use of the + Licensed Material and that the Licensor has authority to license. + . + j. Licensor means the individual(s) or entity(ies) granting rights + under this Public License. + . + k. Share means to provide material to the public by any means or + process that requires permission under the Licensed Rights, such + as reproduction, public display, public performance, distribution, + dissemination, communication, or importation, and to make material + available to the public including in ways that members of the + public may access the material from a place and at a time + individually chosen by them. + . + l. Sui Generis Database Rights means rights other than copyright + resulting from Directive 96/9/EC of the European Parliament and of + the Council of 11 March 1996 on the legal protection of databases, + as amended and/or succeeded, as well as other essentially + equivalent rights anywhere in the world. + . + m. You means the individual or entity exercising the Licensed Rights + under this Public License. Your has a corresponding meaning. + . + . + Section 2 -- Scope. + . + a. License grant. + . + 1. Subject to the terms and conditions of this Public License, + the Licensor hereby grants You a worldwide, royalty-free, + non-sublicensable, non-exclusive, irrevocable license to + exercise the Licensed Rights in the Licensed Material to: + . + a. reproduce and Share the Licensed Material, in whole or + in part; and + . + b. produce, reproduce, and Share Adapted Material. + . + 2. Exceptions and Limitations. For the avoidance of doubt, where + Exceptions and Limitations apply to Your use, this Public + License does not apply, and You do not need to comply with + its terms and conditions. + . + 3. Term. The term of this Public License is specified in Section + 6(a). + . + 4. Media and formats; technical modifications allowed. The + Licensor authorizes You to exercise the Licensed Rights in + all media and formats whether now known or hereafter created, + and to make technical modifications necessary to do so. The + Licensor waives and/or agrees not to assert any right or + authority to forbid You from making technical modifications + necessary to exercise the Licensed Rights, including + technical modifications necessary to circumvent Effective + Technological Measures. For purposes of this Public License, + simply making modifications authorized by this Section 2(a) + (4) never produces Adapted Material. + . + 5. Downstream recipients. + . + a. Offer from the Licensor -- Licensed Material. Every + recipient of the Licensed Material automatically + receives an offer from the Licensor to exercise the + Licensed Rights under the terms and conditions of this + Public License. + . + b. Additional offer from the Licensor -- Adapted Material. + Every recipient of Adapted Material from You + automatically receives an offer from the Licensor to + exercise the Licensed Rights in the Adapted Material + under the conditions of the Adapter's License You apply. + . + c. No downstream restrictions. You may not offer or impose + any additional or different terms or conditions on, or + apply any Effective Technological Measures to, the + Licensed Material if doing so restricts exercise of the + Licensed Rights by any recipient of the Licensed + Material. + . + 6. No endorsement. Nothing in this Public License constitutes or + may be construed as permission to assert or imply that You + are, or that Your use of the Licensed Material is, connected + with, or sponsored, endorsed, or granted official status by, + the Licensor or others designated to receive attribution as + provided in Section 3(a)(1)(A)(i). + . + b. Other rights. + . + 1. Moral rights, such as the right of integrity, are not + licensed under this Public License, nor are publicity, + privacy, and/or other similar personality rights; however, to + the extent possible, the Licensor waives and/or agrees not to + assert any such rights held by the Licensor to the limited + extent necessary to allow You to exercise the Licensed + Rights, but not otherwise. + . + 2. Patent and trademark rights are not licensed under this + Public License. + . + 3. To the extent possible, the Licensor waives any right to + collect royalties from You for the exercise of the Licensed + Rights, whether directly or through a collecting society + under any voluntary or waivable statutory or compulsory + licensing scheme. In all other cases the Licensor expressly + reserves any right to collect such royalties. + . + . + Section 3 -- License Conditions. + . + Your exercise of the Licensed Rights is expressly made subject to the + following conditions. + . + a. Attribution. + . + 1. If You Share the Licensed Material (including in modified + form), You must: + . + a. retain the following if it is supplied by the Licensor + with the Licensed Material: + . + i. identification of the creator(s) of the Licensed + Material and any others designated to receive + attribution, in any reasonable manner requested by + the Licensor (including by pseudonym if + designated); + . + ii. a copyright notice; + . + iii. a notice that refers to this Public License; + . + iv. a notice that refers to the disclaimer of + warranties; + . + v. a URI or hyperlink to the Licensed Material to the + extent reasonably practicable; + . + b. indicate if You modified the Licensed Material and + retain an indication of any previous modifications; and + . + c. indicate the Licensed Material is licensed under this + Public License, and include the text of, or the URI or + hyperlink to, this Public License. + . + 2. You may satisfy the conditions in Section 3(a)(1) in any + reasonable manner based on the medium, means, and context in + which You Share the Licensed Material. For example, it may be + reasonable to satisfy the conditions by providing a URI or + hyperlink to a resource that includes the required + information. + . + 3. If requested by the Licensor, You must remove any of the + information required by Section 3(a)(1)(A) to the extent + reasonably practicable. + . + b. ShareAlike. + . + In addition to the conditions in Section 3(a), if You Share + Adapted Material You produce, the following conditions also apply. + . + 1. The Adapter's License You apply must be a Creative Commons + license with the same License Elements, this version or + later, or a BY-SA Compatible License. + . + 2. You must include the text of, or the URI or hyperlink to, the + Adapter's License You apply. You may satisfy this condition + in any reasonable manner based on the medium, means, and + context in which You Share Adapted Material. + . + 3. You may not offer or impose any additional or different terms + or conditions on, or apply any Effective Technological + Measures to, Adapted Material that restrict exercise of the + rights granted under the Adapter's License You apply. + . + . + Section 4 -- Sui Generis Database Rights. + . + Where the Licensed Rights include Sui Generis Database Rights that + apply to Your use of the Licensed Material: + . + a. for the avoidance of doubt, Section 2(a)(1) grants You the right + to extract, reuse, reproduce, and Share all or a substantial + portion of the contents of the database; + . + b. if You include all or a substantial portion of the database + contents in a database in which You have Sui Generis Database + Rights, then the database in which You have Sui Generis Database + Rights (but not its individual contents) is Adapted Material, + . + including for purposes of Section 3(b); and + c. You must comply with the conditions in Section 3(a) if You Share + all or a substantial portion of the contents of the database. + . + For the avoidance of doubt, this Section 4 supplements and does not + replace Your obligations under this Public License where the Licensed + Rights include other Copyright and Similar Rights. + . + . + Section 5 -- Disclaimer of Warranties and Limitation of Liability. + . + a. UNLESS OTHERWISE SEPARATELY UNDERTAKEN BY THE LICENSOR, TO THE + EXTENT POSSIBLE, THE LICENSOR OFFERS THE LICENSED MATERIAL AS-IS + AND AS-AVAILABLE, AND MAKES NO REPRESENTATIONS OR WARRANTIES OF + ANY KIND CONCERNING THE LICENSED MATERIAL, WHETHER EXPRESS, + IMPLIED, STATUTORY, OR OTHER. THIS INCLUDES, WITHOUT LIMITATION, + WARRANTIES OF TITLE, MERCHANTABILITY, FITNESS FOR A PARTICULAR + PURPOSE, NON-INFRINGEMENT, ABSENCE OF LATENT OR OTHER DEFECTS, + ACCURACY, OR THE PRESENCE OR ABSENCE OF ERRORS, WHETHER OR NOT + KNOWN OR DISCOVERABLE. WHERE DISCLAIMERS OF WARRANTIES ARE NOT + ALLOWED IN FULL OR IN PART, THIS DISCLAIMER MAY NOT APPLY TO YOU. + . + b. TO THE EXTENT POSSIBLE, IN NO EVENT WILL THE LICENSOR BE LIABLE + TO YOU ON ANY LEGAL THEORY (INCLUDING, WITHOUT LIMITATION, + NEGLIGENCE) OR OTHERWISE FOR ANY DIRECT, SPECIAL, INDIRECT, + INCIDENTAL, CONSEQUENTIAL, PUNITIVE, EXEMPLARY, OR OTHER LOSSES, + COSTS, EXPENSES, OR DAMAGES ARISING OUT OF THIS PUBLIC LICENSE OR + USE OF THE LICENSED MATERIAL, EVEN IF THE LICENSOR HAS BEEN + ADVISED OF THE POSSIBILITY OF SUCH LOSSES, COSTS, EXPENSES, OR + DAMAGES. WHERE A LIMITATION OF LIABILITY IS NOT ALLOWED IN FULL OR + IN PART, THIS LIMITATION MAY NOT APPLY TO YOU. + . + c. The disclaimer of warranties and limitation of liability provided + above shall be interpreted in a manner that, to the extent + possible, most closely approximates an absolute disclaimer and + waiver of all liability. + . + . + Section 6 -- Term and Termination. + . + a. This Public License applies for the term of the Copyright and + Similar Rights licensed here. However, if You fail to comply with + this Public License, then Your rights under this Public License + terminate automatically. + . + b. Where Your right to use the Licensed Material has terminated under + Section 6(a), it reinstates: + . + 1. automatically as of the date the violation is cured, provided + it is cured within 30 days of Your discovery of the + violation; or + . + 2. upon express reinstatement by the Licensor. + . + For the avoidance of doubt, this Section 6(b) does not affect any + right the Licensor may have to seek remedies for Your violations + of this Public License. + . + c. For the avoidance of doubt, the Licensor may also offer the + Licensed Material under separate terms or conditions or stop + distributing the Licensed Material at any time; however, doing so + will not terminate this Public License. + . + d. Sections 1, 5, 6, 7, and 8 survive termination of this Public + License. + . + . + Section 7 -- Other Terms and Conditions. + . + a. The Licensor shall not be bound by any additional or different + terms or conditions communicated by You unless expressly agreed. + . + b. Any arrangements, understandings, or agreements regarding the + Licensed Material not stated herein are separate from and + independent of the terms and conditions of this Public License. + . + . + Section 8 -- Interpretation. + . + a. For the avoidance of doubt, this Public License does not, and + shall not be interpreted to, reduce, limit, restrict, or impose + conditions on any use of the Licensed Material that could lawfully + be made without permission under this Public License. + . + b. To the extent possible, if any provision of this Public License is + deemed unenforceable, it shall be automatically reformed to the + minimum extent necessary to make it enforceable. If the provision + cannot be reformed, it shall be severed from this Public License + without affecting the enforceability of the remaining terms and + conditions. + . + c. No term or condition of this Public License will be waived and no + failure to comply consented to unless expressly agreed to by the + Licensor. + . + d. Nothing in this Public License constitutes or may be interpreted + as a limitation upon, or waiver of, any privileges and immunities + that apply to the Licensor or You, including from the legal + processes of any jurisdiction or authority. + . + . + ======================================================================= + . + Creative Commons is not a party to its public licenses. + Notwithstanding, Creative Commons may elect to apply one of its public + licenses to material it publishes and in those instances will be + considered the "Licensor." Except for the limited purpose of indicating + that material is shared under a Creative Commons public license or as + otherwise permitted by the Creative Commons policies published at + creativecommons.org/policies, Creative Commons does not authorize the + use of the trademark "Creative Commons" or any other trademark or logo + of Creative Commons without its prior written consent including, + without limitation, in connection with any unauthorized modifications + to any of its public licenses or any other arrangements, + understandings, or agreements concerning use of licensed material. For + the avoidance of doubt, this paragraph does not form part of the public + licenses. + . + Creative Commons may be contacted at creativecommons.org. diff --git a/debian/gbp.conf b/debian/gbp.conf new file mode 100644 index 000000000..3b6ef2514 --- /dev/null +++ b/debian/gbp.conf @@ -0,0 +1,11 @@ +[DEFAULT] +debian-branch = main +debian-tag = v%(version)s +debian-tag-msg = %(pkg)s v%(version)s + +[tag] +sign-tags = true + +[dch] +postedit = sed -i s"@^\( \+version: '\)[0-9][^']\+\(',\)@\1$GBP_DEBIAN_VERSION\2@" meson.build +ignore-regex = ((Signed-off|Acked)-by:|Part-of:) diff --git a/debian/gir1.2-phosh-0-dev.install b/debian/gir1.2-phosh-0-dev.install new file mode 100644 index 000000000..145460a4b --- /dev/null +++ b/debian/gir1.2-phosh-0-dev.install @@ -0,0 +1 @@ +usr/share/gir-1.0/Phosh-0.gir diff --git a/debian/libphosh-0.45-0.install b/debian/libphosh-0.45-0.install new file mode 100644 index 000000000..fa9236a8d --- /dev/null +++ b/debian/libphosh-0.45-0.install @@ -0,0 +1 @@ +usr/lib/*/libphosh-0.??.so.0 diff --git a/debian/libphosh-0.45-0.symbols b/debian/libphosh-0.45-0.symbols new file mode 100644 index 000000000..9f42c5d30 --- /dev/null +++ b/debian/libphosh-0.45-0.symbols @@ -0,0 +1,134 @@ +libphosh-0.45.so.0 libphosh-0.45-0 #MINVER# +* Build-Depends-Package: libphosh-0.45-dev + LIBPHOSH_0_45_0@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_get_model@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_get_type@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_has_filter@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_new@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_new_for_type@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_refilter@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_set_filter_func@LIBPHOSH_0_45_0 0.45~rc1 + gtk_filter_list_model_set_model@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_get_model@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_get_type@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_has_sort@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_new@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_new_for_type@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_resort@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_set_model@LIBPHOSH_0_45_0 0.45~rc1 + gtk_sort_list_model_set_sort_func@LIBPHOSH_0_45_0 0.45~rc1 + phosh_dbus_screenshot_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_dbus_screenshot_proxy_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_dbus_screenshot_skeleton_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_layer_surface_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_add_extra_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_clear_pin_entry@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_get_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_get_pin_entry@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_get_active_time@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_get_locked@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_get_lockscreen@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_get_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_set_locked@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_manager_set_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_page_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_set_default_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_set_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_set_unlock_status@LIBPHOSH_0_45_0 0.45~rc1 + phosh_lockscreen_shake_pin_entry@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_get_fractional_scale@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_manager_apply_monitor_config@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_manager_get_night_light_supported@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_manager_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_monitor_manager_set_monitor_scale@LIBPHOSH_0_45_0 0.45~rc1 + phosh_notification_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_notify_manager_add_shell_notification@LIBPHOSH_0_45_0 0.45~rc1 + phosh_notify_manager_get_default@LIBPHOSH_0_45_0 0.45~rc1 + phosh_notify_manager_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_active@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_can_show_status@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_long_press_action_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_long_press_action_target@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_showing_status@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_status_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_active@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_can_show_status@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_long_press_action_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_long_press_action_target@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_showing_status@LIBPHOSH_0_45_0 0.45~rc1 + phosh_quick_setting_set_status_page@LIBPHOSH_0_45_0 0.45~rc1 + phosh_screenshot_manager_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_screenshot_manager_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_screenshot_manager_take_screenshot@LIBPHOSH_0_45_0 0.45~rc1 + phosh_session_manager_inhibit@LIBPHOSH_0_45_0 0.45~rc1 + phosh_session_manager_uninhibit@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_fade_out@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_default@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_launcher_entry_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_locked@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_lockscreen_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_lockscreen_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_monitor_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_primary_monitor@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_screenshot_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_session_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_usable_area@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_wifi_manager@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_get_wwan@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_shell_set_default@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_get_extra_widget@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_get_icon_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_get_icon_size@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_get_info@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_set_extra_widget@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_set_icon_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_set_icon_size@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_icon_set_info@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_get_footer@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_get_header@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_get_title@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_placeholder_get_icon_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_placeholder_get_title@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_placeholder_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_placeholder_set_icon_name@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_placeholder_set_title@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_set_footer@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_set_header@LIBPHOSH_0_45_0 0.45~rc1 + phosh_status_page_set_title@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_append_to_strv@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_calculate_supported_mode_scales@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_data_uri_to_pixbuf@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_escape_markup@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_file_equal@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_gesture_is_touch@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_get_icon_by_wifi_strength@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_have_gnome_software@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_matches_app_info@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_open_settings_panel@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_remove_from_strv@LIBPHOSH_0_45_0 0.45~rc1 + phosh_util_toggle_style_class@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_get_clock@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_get_default@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_get_type@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_local_date@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_new@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_set_default@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wall_clock_string_for_datetime@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wifi_manager_get_enabled@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wifi_manager_get_state@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wifi_manager_is_hotspot_master@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wifi_manager_set_hotspot_master@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wwan_has_data@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wwan_is_enabled@LIBPHOSH_0_45_0 0.45~rc1 + phosh_wwan_set_data_enabled@LIBPHOSH_0_45_0 0.45~rc1 diff --git a/debian/libphosh-0.45-dev.install b/debian/libphosh-0.45-dev.install new file mode 100644 index 000000000..879deb648 --- /dev/null +++ b/debian/libphosh-0.45-dev.install @@ -0,0 +1,3 @@ +usr/include/libphosh-0.??/ +usr/lib/*/libphosh-0.??.so +usr/lib/*/pkgconfig/libphosh-0.??.pc diff --git a/debian/not-installed b/debian/not-installed new file mode 100644 index 000000000..231a07da3 --- /dev/null +++ b/debian/not-installed @@ -0,0 +1,3 @@ +usr/include/gmobile/ +usr/lib/*/libphosh-0.??.a +usr/lib/*/girepository-1.0/Phosh-0.typelib diff --git a/debian/phosh-common.install b/debian/phosh-common.install new file mode 100644 index 000000000..803f5ce87 --- /dev/null +++ b/debian/phosh-common.install @@ -0,0 +1,3 @@ +usr/lib/systemd/user/mobi.phosh.OSK.target +usr/share/glib-2.0/schemas/mobi.phosh.shell.enums.xml +usr/share/glib-2.0/schemas/mobi.phosh.shell.gschema.xml diff --git a/debian/phosh-dev.install b/debian/phosh-dev.install new file mode 100644 index 000000000..eec528ae0 --- /dev/null +++ b/debian/phosh-dev.install @@ -0,0 +1,2 @@ +usr/include/phosh/ +usr/lib/*/pkgconfig/phosh*.pc diff --git a/debian/phosh-doc.install b/debian/phosh-doc.install new file mode 100644 index 000000000..64038755f --- /dev/null +++ b/debian/phosh-doc.install @@ -0,0 +1 @@ +usr/share/doc/phosh-0 /usr/share/doc/phosh-doc/ diff --git a/debian/phosh-doc.links b/debian/phosh-doc.links new file mode 100644 index 000000000..47b8a47cc --- /dev/null +++ b/debian/phosh-doc.links @@ -0,0 +1 @@ +usr/share/doc/phosh-doc/phosh-0 usr/share/devhelp/books/phosh-0 diff --git a/debian/phosh-mobile-tweaks.install b/debian/phosh-mobile-tweaks.install new file mode 100644 index 000000000..ff5939789 --- /dev/null +++ b/debian/phosh-mobile-tweaks.install @@ -0,0 +1 @@ +usr/share/glib-2.0/schemas/*.override diff --git a/debian/phosh-plugins.install b/debian/phosh-plugins.install new file mode 100644 index 000000000..df1c724d1 --- /dev/null +++ b/debian/phosh-plugins.install @@ -0,0 +1,6 @@ +usr/share/glib-2.0/schemas/sm.puri.phosh.plugins.*.gschema.xml +usr/share/glib-2.0/schemas/mobi.phosh.plugins.*.gschema.xml +usr/lib/*/phosh/plugins/libphosh-plugin-*.so +usr/lib/*/phosh/plugins/prefs/libphosh-plugin-prefs-*.so +usr/lib/*/phosh/plugins/*.plugin +usr/share/phosh/plugins/icons/*.svg diff --git a/debian/phosh.dirs b/debian/phosh.dirs new file mode 100644 index 000000000..090ecd0b3 --- /dev/null +++ b/debian/phosh.dirs @@ -0,0 +1,3 @@ +/usr/share/phosh +/usr/lib/${DEB_HOST_MULTIARCH}/phosh/plugins +/usr/lib/${DEB_HOST_MULTIARCH}/phosh/plugins/prefs diff --git a/debian/phosh.docs b/debian/phosh.docs new file mode 100644 index 000000000..edc007104 --- /dev/null +++ b/debian/phosh.docs @@ -0,0 +1 @@ +NEWS diff --git a/debian/phosh.install b/debian/phosh.install new file mode 100644 index 000000000..103ee96fb --- /dev/null +++ b/debian/phosh.install @@ -0,0 +1,17 @@ +usr/bin/phosh-session +usr/lib/systemd/user/gnome-session@phosh.target.d/session.conf +usr/lib/systemd/user/mobi.phosh.Shell.service +usr/lib/systemd/user/mobi.phosh.Shell.target +usr/libexec/phosh +usr/libexec/phosh-searchd +usr/libexec/phosh-calendar-server +usr/share/icons/hicolor/symbolic/apps/mobi.phosh.Shell-symbolic.svg +usr/share/locale +usr/share/wayland-sessions/phosh.desktop +usr/share/phosh/phoc.ini +usr/share/gnome-session/sessions/phosh.session +usr/share/applications/mobi.phosh.Shell.desktop +usr/share/dbus-1/services/mobi.phosh.Shell.CalendarServer.service +usr/share/dbus-1/services/mobi.phosh.Shell.Search.service +usr/share/xdg-desktop-portal/phosh-portals.conf +usr/share/xdg-desktop-portal/portals/phosh-shell.portal diff --git a/debian/phosh.manpages b/debian/phosh.manpages new file mode 100644 index 000000000..7e6016eb8 --- /dev/null +++ b/debian/phosh.manpages @@ -0,0 +1,3 @@ +usr/share/man/man1/phosh.1 +usr/share/man/man1/phosh-session.1 +usr/share/man/man5/phosh.gsettings.5 diff --git a/debian/phosh.postinst.in b/debian/phosh.postinst.in new file mode 100644 index 000000000..fc3790d4e --- /dev/null +++ b/debian/phosh.postinst.in @@ -0,0 +1,14 @@ +#!/bin/sh + +set -e + +if [ "$1" = triggered ]; then + "/usr/lib/#MULTIARCH#/glib-2.0/gio-querymodules" "/usr/lib/#MULTIARCH#/phosh/plugins" || true + "/usr/lib/#MULTIARCH#/glib-2.0/gio-querymodules" "/usr/lib/#MULTIARCH#/phosh/plugins/prefs" || true + exit 0 +fi + +#DEBHELPER# + +"/usr/lib/#MULTIARCH#/glib-2.0/gio-querymodules" "/usr/lib/#MULTIARCH#/phosh/plugins" || true +"/usr/lib/#MULTIARCH#/glib-2.0/gio-querymodules" "/usr/lib/#MULTIARCH#/phosh/plugins/prefs" || true diff --git a/debian/phosh.postrm.in b/debian/phosh.postrm.in new file mode 100644 index 000000000..8121e950a --- /dev/null +++ b/debian/phosh.postrm.in @@ -0,0 +1,13 @@ +#! /bin/sh +set -e + +#DEBHELPER# + +case "$1" in + (remove|purge) + if [ -d /usr/lib/#MULTIARCH#/phosh/plugins ]; then + rm -f /usr/lib/#MULTIARCH#/phosh/plugins/giomodule.cache + rm -f /usr/lib/#MULTIARCH#/phosh/plugins/prefs/giomodule.cache + fi + ;; +esac diff --git a/debian/phosh.triggers.in b/debian/phosh.triggers.in new file mode 100644 index 000000000..5484dd9b8 --- /dev/null +++ b/debian/phosh.triggers.in @@ -0,0 +1 @@ +interest-noawait /usr/lib/#MULTIARCH#/phosh/plugins diff --git a/debian/rules b/debian/rules new file mode 100755 index 000000000..b57fe3d80 --- /dev/null +++ b/debian/rules @@ -0,0 +1,47 @@ +#!/usr/bin/make -f + +export DEB_BUILD_MAINT_OPTIONS = hardening=+all + +CONFIGURE_OPTS=--wrap-mode=default +CONFIGURE_OPTS+=-Dsearchd=true + +ifeq ($(filter nodoc,$(DEB_BUILD_PROFILES)),) + CONFIGURE_OPTS+=-Dgtk_doc=true -Dman=true +else + CONFIGURE_OPTS+=-Dgtk_doc=false -Dman=false +endif + +ifeq ($(filter nocheck,$(DEB_BUILD_OPTIONS)),) + CONFIGURE_OPTS+=-Dtests=true +else + CONFIGURE_OPTS+=-Dtests=false +endif + +ifeq ($(filter pkg.phosh.nolib,$(DEB_BUILD_OPTIONS)),) + CONFIGURE_OPTS+=-Dbindings-lib=true +else + CONFIGURE_OPTS+=-Dbindings-lib=false +endif + +%: + dh $@ + +override_dh_auto_configure: + dh_auto_configure -- $(CONFIGURE_OPTS) + +override_dh_auto_install: + set -e; for script in postinst postrm triggers; do \ + sed -e"s/#MULTIARCH#/$(DEB_HOST_MULTIARCH)/g" \ + -e"s/#ARCH#/$(DEB_HOST_ARCH)/g" \ + debian/phosh.$$script.in \ + > debian/phosh.$$script ; \ + done + dh_auto_install + +override_dh_installsystemd: + rm -f debian/phosh.service + cp data/phosh.service debian/phosh.service + dh_installsystemd --no-start --no-enable --no-restart-on-upgrade --no-restart-after-upgrade + +override_dh_auto_test: + LC_ALL=C.UTF-8 xvfb-run -a dh_auto_test -- --no-suite manual diff --git a/debian/source/format b/debian/source/format new file mode 100644 index 000000000..89ae9db8f --- /dev/null +++ b/debian/source/format @@ -0,0 +1 @@ +3.0 (native) diff --git a/debian/tests/control b/debian/tests/control new file mode 100644 index 000000000..b7bccb552 --- /dev/null +++ b/debian/tests/control @@ -0,0 +1,7 @@ +Test-Command: /usr/libexec/phosh --help +Restrictions: superficial +Depends: phosh + +Test-Command: pkg-config --libs --cflags libphosh-0.45 +Restrictions: superficial +Depends: libphosh-0.45-dev diff --git a/docs/app-dev.md b/docs/app-dev.md new file mode 100644 index 000000000..f4135b766 --- /dev/null +++ b/docs/app-dev.md @@ -0,0 +1,48 @@ +Title: Notes for Application Developers +Slug: appdev + +# Notes for application development + +Phosh relies on freedesktop.org and Wayland protocols where +possible. Sometimes we have phosh specific extensions though until +changes made it into the spec. + +## Desktop Entry Specification + +Phosh aims to parse app entries according to the [Desktop Entry +Specification][]. For some mobile needs phosh honors the following +additional entries: + +* `X-Purism-FormFactor`: Allows app to specify that they are suitable for use + on phones. Example: `X-Purism-FormFactor=Workstation;Mobile;`. Note that this + will be phased out in favour of supplying this information + [via appstream metadata][]. +* `X-KDE-FormFactor`: Similar to the above (used by some KDE applications) +* `X-Geoclue-Reason`: Used to indicate the reason why an application + wants to access location data. +* `X-Phosh-Lockscreen-Actions`: Used to allow actions to be used on the lock screen. + Example: `X-Phosh-Lockscreen-Actions=app.stop-alarm::;app.snooze-alarm::;` +* `X-Phosh-UsesFeedback`: Used to indicate that application submit events to + feedbackd to trigger audio, haptic or led feedback. This is useful for setting + applications so they can show entries for individual apps. + Example: `X-Phosh-UsesFeedback=true` +* `X-GNOME-UsesNotifications`: Used to indicate that application submit notifications + This is useful for setting applications so they can show entries for + individual apps to fine tune their feedback. + Example: `X-GNOME-UsesNotifications=true` + +## Desktop Notification Specification + +Phosh's notification server aims to adhere to the [Desktop Notification +Specification][]. On mobile we need some extensions to handle haptic and led feedback +for those notifications. So phosh honors the following additional hint: + +* `x-phosh-fb-profile`: The feedback profile to use for events associated with + this notification. See the [Feedback Theme Specification][] for details. The + additional value `none` is used to indicate that the event that would be + triggered should be suppressed and hence no feedback emitted to the user. + +[Desktop Entry Specification]: https://specifications.freedesktop.org/desktop-entry-spec/latest/ +[Desktop Notification Specification]: https://specifications.freedesktop.org/notification-spec/notification-spec-latest.html#hints +[Feedback Theme Specification]: https://honk.sigxcpu.org/projects/feedbackd/doc/Feedback-theme-spec-0.0.0.html +[via appstream metadata]: https://gitlab.gnome.org/World/Phosh/phosh/-/merge_requests/950 diff --git a/docs/gettingstarted.md b/docs/gettingstarted.md new file mode 100644 index 000000000..a7c68e706 --- /dev/null +++ b/docs/gettingstarted.md @@ -0,0 +1,192 @@ +Title: Getting started with Phosh development +Slug: gettingstarted + +# Getting started with Phosh development + +## Overview + +Phosh is a graphical shell for +[Wayland](https://wayland.freedesktop.org/). It's target at mobile +devices with small screens like Purism's Librem 5 running adaptive +[GNOME](https://gitlab.gnome.org/) applications. + +It's purpose is to provide a graphical user interface to launch +applications, display status information (time, battery status, ...), +to provide a lock screen and to make often used functionality quickly +accessible. It's also meant to provide a proper interface when either +connecting a keyboard and monitor to a phone or (but to a lesser +extend) when running on a laptop. + +Since it acts as a Wayland client it needs a compositor to function +that provides the necessary protocols (most notably +wlr-layer-shell). It's usually used with +[phoc](https://gitlab.gnome.org/World/Phosh/phoc) (the PHOne Compositor). + +On the GNOME side it interfaces with the usual components +(e.g. [gnome-settings-daemon](https://gitlab.gnome.org/GNOME/gnome-settings-daemon), +[upower](https://gitlab.freedesktop.org/upower/upower), +[iio-sensor-proxy](https://gitlab.freedesktop.org/hadess/iio-sensor-proxy/)) +via DBus. For haptic feedback it uses +[feedbackd](https://source.puri.sm/Librem5/feedbackd/). + +It uses [GTK](https://www.gtk.org/) as it's GUI toolkit and +[libhandy](https://gitlab.gnome.org/GNOME/libhandy) for adaptive +widgets. + +Although targeted at touch devices Phosh does not implement a +on screen keyboard (OSK) but leaves this to separate on-screen +keyboards like [stevia](https://gitlab.gnome.org/World/Phosh/stevia) +or [squeekboard](https://gitlab.gnome.org/World/Phosh/squeekboard). + +The above combination of software is also often (a bit imprecisely) +named Phosh. For a high level overview see [Phosh Overview](https://honk.sigxcpu.org/con/phosh_overview.html). + +### Wayland protocols + +Since Phoc (in contrast to some other solutions) aims to be a minimal +Wayland compositor that manages rendering, handle physical and virtual +input and display devices but not much more it needs to provide some +more protocols to enable graphical shells like Phosh. These are usually +from [wlr-protocols](https://github.com/swaywm/wlr-protocols) and +Phoc uses the [wlroots](https://github.com/swaywm/wlroots) library for +implementing them. + +These are the most prominent ones use by Phosh: + +- wlr-layer-shell: Usually Wayland clients have little influence on where + the compositor places them. This protocol gives Phosh enough room + to build the top bar via #PhoshTopPanel, the home bar #PhoshHome at + the bottom, system modal dialogs e.g. #PhoshSystemPrompt and + lock screens via #PhoshLockscreen. +- wlr-foreign-toplevel-management: This allows the management of + toplevels (windows). Phosh uses this to build an application switcher + called #PhoshOverview. +- wlr-output-management: This allows to manage a monitors power + state (to e.g.turn it off when unused). + +Besides those Phosh uses a number of "regular" Wayland client +protocols like `xdg_output`, `wl_output` or `wl_seat` (see +`PhoshWayland` for the full list). + +### Session startup + +Since Phosh is in many aspects a regular GTK application it's started +as part GNOME session so the start sequence looks like + +``` +phoc (compositor) -> gnome-session -> phosh (and other session components) +``` + +## Running and Testing Phosh + +The following might useful when you want to make changes to Phosh and +test the changes: + +### Running phosh + +For development purposes you can run phosh nested on your desktop. See +[this blog post](https://phosh.mobi/posts/phosh-dev-part-0/) for +details. You can use several helper scripts to test the running +instance: + +### Checking DBus Interfaces + +The `tools/` directory contains short snippets to test various DBus interfaces +e.g. `check-osd` to test the OSD overlay (`PhoshOsdWindow`) or `check-screenshot` +to check the screenshot API (`PhoshScreenshotManager`). + +### Mocking DBus Services + +To mock DBus services used by phosh like `org.freedesktop.ModemManager` +or `net.hadess.SensorProxy` you can use [python-dbusmock][]. + +`tests/mock-mm-nm.py` uses this to mock `ModemManager` and +`NetworkManager` to simulate a mobile data connection: + +``` +sudo tests/mock-mm-nm.py +``` + +Make sure you stop `ModemManager` and `NetworkManager` before running +the above. + +You can also use the mocks from `python-dbusmock` directly, e.g. to mock +settings daemon's rfkill state: + +``` +python3 -m dbusmock --template gsd_rfkill +``` + +to e.g. mock `gsd-rfkill`. To simulate airplane mode you could use: + +``` +gdbus call --session -d org.gnome.SettingsDaemon.Rfkill \ + -o /org/gnome/SettingsDaemon/Rfkill \ + -m org.freedesktop.DBus.Mock.SetAirplaneMode true +``` + +### Integration tests + +#### Running with DBus mocks + +We have some tests checking integration with system services like +ModemManager using python-dbusmock. You can run them using + +```sh +_build/tests/integration/run-pytest -v -s tests/integration/test_dbus.py +``` + +If you want to inspect phosh's log output use + +```sh +SAVE_TEST_LOGS=1 _build-test/tests/integration/run-pytest -s tests/integration/test_dbus.py +``` + +and `stderr` and `stdout` will be written into `log.stderr` and +`log.stdout` in the current directory. + +#### Running the Screenshot Tests + +One testcase brings up different parts of the shell and takes screenshots from them +to ease finding visual regressions. You can run them locally via + +```sh +PHOSH_TEST_TYPE=doesnotmatter _build-test/tests/test-take-screenshots +``` + +### GTK Inspector + +Since phosh is a GTK application you can use +[GtkInspector](https://developer.gnome.org/documentation/tools/inspector.html). +You can use the `GTK_INSPECTOR_DISPLAY` environment variable to use a different +Wayland display for the inspector window. This can be useful to have the +inspector windows outside of a nested Wayland session. + +## Development Hints + +### Manager Objects + +Phosh uses several manager objects e.g. `PhoshBackgroundManager`, +`PhoshMonitorManager`, `PhoshLockscreenManager` to keep track +of certain objects (monitors, lock screens, backgrounds) and to +trigger events on those when needed. They're usually created and +disposed by #PhoshShell. Some of them like +`PhoshWayland` are singletons so you can access them from basically +anywhere in the codebase. + +### Status Information Widgets + +Several widgets listen to changes on DBus objects in order to e.g. display the +current connectivity - see #PhoshConnectivityInfo for an example that monitors +network connectivity. + +Sometimes it is no longer useful to show the widget (since +e.g. the corresponding DBus service went away). In that case the +widget should flip a boolean property so the parent container can +hide the object via #g_object_bind_property(). + +### Screen Locking and Blanking + +For details see PhoshScreenSaverManager. + +[python-dbusmock]: https://github.com/martinpitt/python-dbusmock diff --git a/docs/meson.build b/docs/meson.build new file mode 100644 index 000000000..be9aa9224 --- /dev/null +++ b/docs/meson.build @@ -0,0 +1,77 @@ +if get_option('gtk_doc') + + expand_content_md_files = [ + 'app-dev.md', + 'gettingstarted.md', + 'phosh-dbus-sm.puri.OSK0.md', + ] + + toml_data = configuration_data() + toml_data.set('VERSION', meson.project_version()) + + phosh_toml = configure_file( + input: 'phosh.toml.in', + output: 'phosh.toml', + configuration: toml_data, + ) + + dependency( + 'gi-docgen', + version: '>= 2021.1', + fallback: ['gi-docgen', 'dummy_dep'], + native: true, + required: get_option('gtk_doc'), + ) + + gidocgen = find_program('gi-docgen') + + docs_dir = datadir / 'doc' + + custom_target( + 'phosh-doc', + input: [phosh_toml, phosh_gir[0]], + output: 'phosh-0', + command: [ + gidocgen, + 'generate', + '--quiet', + '--fatal-warnings', + '--add-include-path=@0@'.format(meson.current_build_dir() / '../src'), + '--config=@INPUT0@', + '--output-dir=@OUTPUT@', + '--no-namespace-dir', + '--content-dir=@0@'.format(meson.current_source_dir()), + '@INPUT1@', + ], + depend_files: [expand_content_md_files], + build_by_default: true, + install: true, + install_dir: docs_dir, + ) + +endif + +if get_option('man') + manpages = [['phosh', 1], ['phosh.gsettings', 5], ['phosh-session', 1]] + + rst2man = find_program('rst2man', 'rst2man.py', required: false) + if not rst2man.found() + error('rst2man is required to build the manpages') + endif + rst2man_flags = ['--syntax-highlight=none'] + + foreach manpage : manpages + man_name = manpage[0] + man_section = manpage[1] + + custom_target( + 'man-@0@'.format(man_name), + input: '@0@.rst'.format(man_name), + output: '@0@.@1@'.format(man_name, man_section), + command: [rst2man, rst2man_flags, '@INPUT@'], + capture: true, + install: true, + install_dir: get_option('mandir') / 'man@0@'.format(man_section), + ) + endforeach +endif diff --git a/docs/phosh-dbus-sm.puri.OSK0.md b/docs/phosh-dbus-sm.puri.OSK0.md new file mode 100644 index 000000000..04c8e286d --- /dev/null +++ b/docs/phosh-dbus-sm.puri.OSK0.md @@ -0,0 +1,39 @@ +Title: sm.puri.OSK0 DBus Interface +Slug: sm.puri.OSK0 + +# sm.puri.OSK0 + +## Description + +This interface is exported by on screen keyboards (OSK) to +indicate and set state like visibility. + +### Properties + +#### sm.puri.OSK0:Visible + +``` + Visible readable b +``` + +Indicates whether the on screen keyboard is currently +unfolded. `TRUE` if the OSK is currently visible to the user, +`FALSE` otherwise. + +### Methods + +#### sm.puri.OSK0.SetVisible + +``` + SetVisible ( + IN visible b + ) +``` + +Toggle keyboard visibility. + +* visible: `TRUE` if the OSK should be shown to the user, `FALSE` otherwise + +# See also + +* [Phosh's On Screen Keyboard Interface](https://phosh.mobi/posts/phosh-osk-interface/) diff --git a/docs/phosh-session.rst b/docs/phosh-session.rst new file mode 100644 index 000000000..bff171789 --- /dev/null +++ b/docs/phosh-session.rst @@ -0,0 +1,56 @@ +.. _phosh-session(1): + +============= +phosh-session +============= + +-------------------------------- +Session startup script for phosh +-------------------------------- + +SYNOPSIS +-------- +| **phosh-session** [OPTIONS...] + + +DESCRIPTION +----------- + +``phosh-session`` is the script to startup phosh including required +components like the Wayland compositor ``phoc`` and an on screen +keyboard like ``stevia`` or ``squeekboard``. The script is rarely invoked by a +user but rather activated by a display manager or systemd unit. However it's +perfectly valid to e.g. log into a tty and run ``phosh-session`` to bring up +phosh and needed components. + +OPTIONS +------- + +``-h``, ``--help`` + Print help and exit + +``--version`` + Print version and exit + +CONFIGURATION FILES +------------------- +The session script looks at the following configuration files: + +- ``/usr/share/phosh/phoc.ini``: Configuration passed to the compositor +- ``/etc/phosh/phoc.ini``: Used instead of the above if present + +ENVIRONMENT VARIABLES +--------------------- + +``phosh-session`` honors the following environment variables: + +- ``WLR_BACKENDS``: The backends the wlroots library should use when phoc launches. See + https://gitlab.freedesktop.org/wlroots/wlroots/-/blob/master/docs/env_vars.md + +For debugging purposes you can put environment variables into +``~/.phoshdebug`` which is read at session startup. + +See also +-------- + +``phosh(1)`` ``phoc(1)`` ``phoc.ini(5)`` ``phosh-osk-stevia(1)`` diff --git a/docs/phosh.gsettings.rst b/docs/phosh.gsettings.rst new file mode 100644 index 000000000..729e77c5b --- /dev/null +++ b/docs/phosh.gsettings.rst @@ -0,0 +1,144 @@ +.. _phosh.gsettings(5): + +=============== +phosh.gsettings +=============== + +----------------------- +GSettings used by phosh +----------------------- + +DESCRIPTION +----------- + +Phosh uses gsettings for most of its configuration. You can use the ``gsettings(1)`` command +to change them or to get details about the option. To get more details about an option use +`gsettings describe`, e.g.: + +:: + + gsettings describe mobi.phosh.shell.cell-broadcast enabled + +These are the currently used gsettings schema and keys: + +GSettings +~~~~~~~~~ + +These gsettings are used by ``phosh``: + +- `mobi.phosh.shell.brightness` + + - `auto-brightness-offset`: Offset added to the calculated auto brightness value + +- `mobi.phosh.shell.cell-broadcast` + + - `enabled`: Whether receiving cell broadcasts is enabled + +- `sm.puri.phosh` + + - `app-filter-mode`: Application filtering in overview. This shows all applications: + + :: + + gsettings set sm.puri.phosh app-filter-mode '[]' + + - `automatic-high-contrast-threshold`: The brightness threshold to trigger automatic high contrast + - `automatic-high-contrast`: Whether automatic high contrast is enabled + - `enable-suspend`: Whether suspend is enabled in the system menu: + - `favorites`: List of favorite applications + - `force-adaptive`: List of app-id's phosh considers adaptive (so they are always shown): + + :: + + gsettings set sm.puri.phosh force-adaptive "['chromium.desktop', 'firefox-esr.desktop']" + + - `osk-unfold-delay`: How long to press bottom bar pill for OSK to unfold + - `quick-silent`: Silence device by pression Vol- on incoming calls + - `shell-layout`: How to layout shell UI elements around cutouts and notches. + `device`: Automatic based on device settings, `none`: No automatic layout. + - `wwan-backend`: WWan backend to use + +- `sm.puri.phosh.emergency-calls` + + - `enabled`: Whether emergency calls are enabled. This is + experimental, please check if it works for your povider / in your + country / for your sim card before relying on it. + +- `sm.puri.phosh.lockscreen` + + - `require-unlock`: Whether unlock via password/pin is required + - `shuffle-keypad`: Shuffle keys on keypad + +- `sm.puri.phosh.plugins` + + - `lockscreen`: The enabled lock screen plugins + - `quick-settings`: The enabled custom quick settings + +- `sm.puri.phosh.notifications` + + - `wakeup-screen-categories`: Notification categories that trigger screen wakeup + - `wakeup-screen-triggers`: What properties of a notification trigger screen wake-up + - `wakeup-screen-urgency`: The lowest urgency that triggers screen wake-up (if via `wakeup-screen-triggers`) + +- `sm.puri.phosh.plugins.launcher-box` + + - `folder`: The folder containing the desktop files to show in the launcher box + +- `sm.puri.phosh.plugins.ticket-box` + + - `folder`: The folder containing the tickets to be presented on the lock screen. + +- `sm.puri.phosh.plugins.upcoming-events` + + - `days`: Number of days to show in the upcoming events list + +- `org.gnome.desktop.app-folders.folder`: Folder support + +- `org.gnome.desktop.background`: Background wallpaper + + - `picture-uri` + - `picture-uri-dark` + - `primary-color` + + E.g. to get the "classic" black overview back use: + + :: + + gsettings set org.gnome.desktop.background picture-options none + gsettings set org.gnome.desktop.background primary-color '#000000' + +- `org.gnome.desktop.interface1` + + - `show-battery-percentage` + +- `org.gnome.desktop.media-handling` + + - `automount` + +- `org.gnome.desktop.notifications` + + - `show-banners` : Whether to show notification banners + - `show-in-lockscreen` : Whether notifications should be shown on the lock screen + +- `org.gnome.desktop.peripherals.touchscreen`: Touch screen to output mappings. Example for NexDock 360: + + :: + + gsettings set org.gnome.desktop.peripherals.touchscreen:/org/gnome/desktop/peripherals/touchscreens/27c0:0819/ output "\['Unknown', 'NexDock', '8R33926O00Q'\]" + +- `org.gnome.desktop.screensaver`: + + - `lock-delay` + - `lock-enabled` + - `picture-uri`: Lockscreen background image / wallpaper + - `picture-options`: `none` disable lockscreen image + +- `org.gnome.shell.keybindings` + + - `toggle-message-tray` + - `toggle-overview` + +See also +-------- + +``phosh(1)`` ``phoc.gsettings(5)`` ``gsettings(1)`` diff --git a/docs/phosh.rst b/docs/phosh.rst new file mode 100644 index 000000000..06960d205 --- /dev/null +++ b/docs/phosh.rst @@ -0,0 +1,94 @@ +.. _phosh(1): + +===== +phosh +===== + +--------------------- +Phosh - A phone shell +--------------------- + +SYNOPSIS +-------- +| **phosh** [OPTIONS...] + + +DESCRIPTION +----------- + +``phosh`` is a Wayland shell for mobile devices using GNOME technologies. + +OPTIONS +------- + +``-h``, ``--help`` + Print help and exit + +``-U``, ``--unlocked`` + Don't start with screen locked + +``-L``, ``--locked`` + Start with screen locked, no matter what + +``--version`` + Show version information + +CONFIGURATION +------------- + +``phosh`` is configured via ``GSettings``, see ``phosh-config(5)``. + +Plugins +^^^^^^^ + +Plugins are configured via the ``mobi.phosh.plugins`` and ``sm.puri.phosh.plugins`` gsettings +schema. The ``lock-screen`` key enables the plugins on the lock screen +e.g. + +:: + + gsettings set sm.puri.phosh.plugins lock-screen "['ticket-box', 'upcoming-events']" + +DBUS INTERFACE +-------------- + +``phosh`` allows to enable and disable certain debug flags at runtime via DBus. To see a list of +available flags use: + +:: + + busctl --user introspect mobi.phosh.Shell.DebugControl /mobi/phosh/Shell/DebugControl mobi.phosh.Shell.DebugControl + +To toggle individual values: + +:: + + busctl --user set-property mobi.phosh.Shell.DebugControl /mobi/phosh/Shell/DebugControl mobi.phosh.Shell.DebugControl LogDomains as 2 phosh-shell phosh-brightness-manager + +Note that the flags are not considered stable API so can change +between releases. + + +ENVIRONMENT VARIABLES +--------------------- + +``phosh`` honors the following environment variables for debugging purposes: + +- ``PHOSH_DEBUG``: A comma separated list of flags: + + - ``always-splash``: Always use splash screen when starting apps + (even when in docked mode) + - ``fake-builtin``: Fake a builtin screen when using a virtual output like + in a nested Wayland session. +- ``PHOSH_FAKE_CLOCK``: Allowed values are ISO8601 formatted strings + or ``now``. Setting this variable sets the shell's clocs to the + given fixed value. For the clock format see ``g_date_time_new_from_iso8601()``. +- ``G_MESSAGES_DEBUG``, ``G_DEBUG`` and other environment variables supported + by glib. https://docs.gtk.org/glib/running.html +- ``GTK_DEBUG`` and other environment variables supported by GTK, see + https://docs.gtk.org/gtk3/running.html + +See also +-------- + +``gsettings(1)`` ``phosh-session(1)`` ``phosh.gsettings(5)`` diff --git a/docs/phosh.toml.in b/docs/phosh.toml.in new file mode 100644 index 000000000..12716752e --- /dev/null +++ b/docs/phosh.toml.in @@ -0,0 +1,56 @@ +[library] +version = "@VERSION@" +description = "Phosh is a Wayland shell for mobile devices using GNOME technologies." +authors = "The Phosh Developers" +license = "GPL-3-or-later" +browse_url = "https://gitlab.gnome.org/World/Phosh/phosh" +repository_url = "https://gitlab.gnome.org/World/Phosh/phosh.git" +website_url = "https://phosh.mobi" +dependencies = [ + "Gio-2.0", + "GLib-2.0", + "Gtk-3.0", + "Libhandy-1.0", +] +devhelp = true +search_index = true + +[dependencies."Gio-2.0"] +name = "GIO" +description = "GObject Interfaces and Objects, Networking, IPC, and I/O" +docs_url = "https://docs.gtk.org/gio/" + +[dependencies."GLib-2.0"] +name = "GLib" +description = "The base type system library" +docs_url = "https://docs.gtk.org/glib/" + +[dependencies."Gtk-3.0"] +name = "GTK" +description = "The GTK toolkit" +docs_url = "https://docs.gtk.org/gtk3/" + +[dependencies."Libhandy-1.0"] +name = "Handy" +description = "Building blocks for modern adaptive GNOME applications." +docs_url = "https://gnome.pages.gitlab.gnome.org/libhandy/" + +[theme] +name = "basic" +show_index_summary = true +show_class_hierarchy = true + +[source-location] +# The base URL for the web UI +base_url = "https://gitlab.gnome.org/World/Phosh/phosh/-/blob/main/" +# The format for links, using "filename" and "line" for the format +file_format = "{filename}#L{line}" + +[extra] +# Order here is the order they're listed in the docs +content_files = [ + "gettingstarted.md", + "phosh-dbus-sm.puri.OSK0.md", + "app-dev.md", +] +urlmap_file = "urlmap.js" diff --git a/docs/urlmap.js b/docs/urlmap.js new file mode 100644 index 000000000..963cb3152 --- /dev/null +++ b/docs/urlmap.js @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 The Phosh Developers +// SPDX-License-Identifier: GPL-3.0-or-later + +baseURLs = [ + [ 'GLib', 'https://docs.gtk.org/glib/' ], + [ 'GObject', 'https://docs.gtk.org/gobject/' ], + [ 'Gio', 'https://docs.gtk.org/gio/' ], + [ 'Gtk', 'https://docs.gtk.org/gtk3/' ] +] diff --git a/gcovr.cfg b/gcovr.cfg new file mode 100644 index 000000000..6f7450863 --- /dev/null +++ b/gcovr.cfg @@ -0,0 +1,21 @@ +# Imported verbatim from gnome-shell +exclude = calendar-server/ +exclude = src/contrib/ + +# Autogenerated +exclude = _build/ +exclude = _build/protocol/ +exclude = _build/plugins/upcoming-events/phosh-plugin-upcoming-events-phosh-calendar-dbus.c +exclude = _build/plugins/upcoming-events/phosh-plugin-upcoming-events-resources.c +exclude = _build/src/phosh-enums.c +exclude = _build/src/phosh-marshalers.c +exclude = _build/src/phosh-resources.c +exclude = _build/subprojects/libcall-ui/src/cui-enums.c +exclude = _build/subprojects/libcall-ui/src/cui-resources.c +# Imported verbatim from GTK +exclude = src/gtk-list-models/ + +exclude = src/libphosh-tool.a.p/ +exclude = src/libphosh.so.p/ +exclude = subprojects/ +exclude = tools/ diff --git a/meson.build b/meson.build new file mode 100644 index 000000000..f7a08c37e --- /dev/null +++ b/meson.build @@ -0,0 +1,296 @@ +project( + 'phosh', + 'c', + version: '0.53.0', + license: 'GPL-3.0-or-later', + meson_version: '>= 1.7.0', + default_options: [ + 'warning_level=1', + 'buildtype=debugoptimized', + 'c_std=gnu11', + ], +) + +# libphosh's version is stable. It should only be bumped for API changes +libphosh_api_version = '0.45' + +app_id = 'mobi.phosh.Shell' +ver_parts = meson.project_version().split('.') +lib_inc_subdir = 'libphosh-@0@'.format(libphosh_api_version) + +prefix = get_option('prefix') +bindir = join_paths(prefix, get_option('bindir')) +datadir = join_paths(prefix, get_option('datadir')) +localedir = join_paths(prefix, get_option('localedir')) +libdir = join_paths(prefix, get_option('libdir')) +libexecdir = join_paths(prefix, get_option('libexecdir')) +desktopdir = join_paths(datadir, 'applications') +sessiondir = join_paths(datadir, 'gnome-session') +pkgdatadir = join_paths(datadir, meson.project_name()) +pkglibdir = join_paths(libdir, meson.project_name()) +systemddir = join_paths(prefix, 'lib/systemd') +systemduserdir = join_paths(systemddir, 'user') +plugins_dir = join_paths(prefix, libdir, 'phosh', 'plugins') +plugin_prefs_dir = join_paths(prefix, libdir, 'phosh', 'plugins', 'prefs') +phosh_plugin_icon_dir = datadir / 'phosh' / 'plugins' / 'icons' +servicedir = join_paths(datadir, 'dbus-1', 'services') +schemasdir = datadir / 'glib-2.0/schemas' +lib_inc_dir = prefix / get_option('includedir') / lib_inc_subdir +vapidir = prefix / datadir / 'vala' / 'vapi' + +enable_introspection = get_option('introspection') or get_option('gtk_doc') +bindings_lib = get_option('bindings-lib') +abi_check = get_option('abi-check') + +if prefix != '/usr' + sysconfdir = join_paths(prefix, get_option('sysconfdir')) +else + sysconfdir = get_option('sysconfdir') +endif + +glib_ver = '2.80' +glib_ver_str = 'GLIB_VERSION_@0@'.format(glib_ver.replace('.', '_')) +glib_ver_cmp = '>=@0@'.format(glib_ver) + +add_project_arguments( + [ + '-DGLIB_VERSION_MIN_REQUIRED=@0@'.format(glib_ver_str), + '-DGLIB_VERSION_MAX_REQUIRED=@0@'.format(glib_ver_str), + '-DG_LOG_USE_STRUCTURED', + '-DGMOBILE_USE_UNSTABLE_API', + '-DLIBFEEDBACK_USE_UNSTABLE_API', + ], + language: 'c', +) + +root_inc = include_directories('.') + +cc = meson.get_compiler('c') + +global_c_args = [] +test_c_args = [ + '-Wcast-align', + '-Wdate-time', + '-Wdeclaration-after-statement', + ['-Werror=format-security', '-Werror=format=2'], + '-Wendif-labels', + '-Werror=incompatible-pointer-types', + '-Werror=missing-declarations', + '-Werror=overflow', + '-Werror=return-type', + '-Werror=shift-count-overflow', + '-Werror=shift-overflow=2', + '-Werror=implicit-fallthrough=3', + '-Wfloat-equal', + '-Wformat-nonliteral', + '-Wformat-security', + '-Winit-self', + '-Wmaybe-uninitialized', + '-Wmissing-field-initializers', + '-Wmissing-include-dirs', + '-Wmissing-noreturn', + '-Wnested-externs', + '-Wno-missing-field-initializers', + '-Wno-sign-compare', + '-Wno-strict-aliasing', + '-Wno-unused-parameter', + '-Wold-style-definition', + '-Wpointer-arith', + '-Wredundant-decls', + '-Wshadow', + '-Wstrict-prototypes', + '-Wswitch-default', + '-Wswitch-enum', + '-Wtype-limits', + '-Wundef', + '-Wunused-function', +] +if get_option('buildtype') != 'plain' + test_c_args += '-fstack-protector-strong' +endif + +foreach arg : test_c_args + if cc.has_multi_arguments(arg) + global_c_args += arg + endif +endforeach + +if cc.get_id() == 'clang' + # Avoid cast align warnings for wl_container_of, etc + global_c_args += '-Wno-cast-align' +endif + +add_project_arguments(global_c_args, language: 'c') + +gnome = import('gnome') +i18n = import('i18n') +pkgconfig = import('pkgconfig') + +appstream_dep = dependency('appstream', version: '>= 1.0.0') +ecal_dep = dependency('libecal-2.0', version: '>= 3.33.1') +eds_dep = dependency('libedataserver-1.2', version: '>= 3.33.1') +fribidi_dep = dependency('fribidi') +gcr_dep = dependency('gcr-3', version: '>= 3.7.5') +glib_dep = dependency('glib-2.0', version: glib_ver_cmp) +gio_dep = dependency('gio-2.0', version: glib_ver_cmp) +gio_unix_dep = dependency('gio-unix-2.0', version: glib_ver_cmp) +gnome_bluetooth_dep = dependency('gnome-bluetooth-3.0', version: '>= 46.0') +gmobile_dep = dependency( + 'gmobile', + version: '>= 0.1.0', + fallback: ['gmobile', 'gmobile_dep'], + default_options: [ + 'examples=false', + 'introspection=false', + 'gtk_doc=false', + 'tests=false', + ], +) +gmodule_dep = dependency('gmodule-no-export-2.0', version: glib_ver_cmp) +gnome_desktop_dep = dependency('gnome-desktop-3.0', version: '>=3.26') +gobject_dep = dependency('gobject-2.0', version: glib_ver_cmp) +gsettings_desktop_schemas_dep = dependency( + 'gsettings-desktop-schemas', + version: '>=47', +) +gtk_dep = dependency('gtk+-3.0', version: '>=3.22') +gtk_wayland_dep = dependency('gtk+-wayland-3.0', version: '>=3.22') +gudev_dep = dependency('gudev-1.0') +libcall_ui = subproject( + 'libcall-ui', + default_options: [ + 'examples=false', + 'gtk_doc=false', + 'tests=false', + 'install-i18n=@0@'.format(get_option('callui-i18n')), + ], +) +libfeedback_dep = dependency( + 'libfeedback-0.0', + version: '>= 0.7.0', + fallback: ['libfeedback', 'libfeedback_dep'], + default_options: [ + 'introspection=disabled', + 'daemon=false', + 'gtk_doc=false', + 'tests=false', + ], +) +libgvc = subproject( + 'gvc', + default_options: [ + 'package_name=' + meson.project_name(), + 'package_version=' + meson.project_version(), + 'pkgdatadir=' + pkgdatadir, + 'pkglibdir=' + pkglibdir, + 'static=true', + 'introspection=false', + 'alsa=false', + ], +) +libgvc_dep = libgvc.get_variable('libgvc_dep') +libhandy_dep = dependency( + 'libhandy-1', + version: '>=1.1.90', + fallback: ['libhandy', 'libhandy_dep'], + default_options: ['introspection=disabled'], +) +libcall_ui_dep = libcall_ui.get_variable('libcall_ui_dep') +libnm_dep = dependency('libnm', version: '>= 1.14') +libpolkit_agent_dep = dependency('polkit-agent-1', version: '>= 0.122') +libsoup_dep = dependency('libsoup-3.0', version: '>= 3.6') +libsystemd_dep = dependency('libsystemd', 'libelogind', version: '>= 241') +mm_glib_dep = dependency('mm-glib', version: '>= 1.24') +network_agent_dep = dependency('libsecret-1') +upower_glib_dep = dependency('upower-glib', version: '>=1.90') +wayland_client_dep = dependency('wayland-client', version: '>=1.14') +wayland_protos_dep = dependency('wayland-protocols', version: '>=1.12') + +code = ''' +#include + +struct rfkill_event_ext e; +''' +have_rfkill_event_ext = cc.compiles(code, name: 'Have rfkill_event_ext') +have_memfd_create = cc.has_function( + 'memfd_create', + prefix: ['#define _GNU_SOURCE', '#include '], +) + +config_h = configuration_data() +config_h.set_quoted('GETTEXT_PACKAGE', 'phosh') +config_h.set_quoted('LOCALEDIR', localedir) +config_h.set_quoted('PHOSH_PLUGINS_DIR', plugins_dir) +config_h.set_quoted('PHOSH_VERSION', meson.project_version()) +config_h.set('PHOSH_ANIMATION_SLOWDOWN', get_option('animation-slowdown')) +config_h.set('HAVE_RFKILL_EVENT_EXT', have_rfkill_event_ext) +config_h.set( + 'PHOSH_HAVE_MEMFD_CREATE', + have_memfd_create, + description: 'Whether we have memdfd_create on Linux', +) +config_h.set( + 'PHOSH_USES_ASAN', + get_option('b_sanitize') == 'address', + description: 'Whether ASAN was enabled via meson', +) +config_h.set_quoted( + 'PHOSH_DATA_DIR', + datadir, + description: 'Systemwide data dir', +) +config_h.set_quoted('PHOSH_APP_ID', app_id) +config_h.set_quoted('PHOSH_DBUS_PATH_PREFIX', '/' + app_id.replace('.', '/')) + +configure_file( + output: 'phosh-config.h', + configuration: config_h, + install: bindings_lib, + install_dir: lib_inc_dir, +) + + +subdir('data') +subdir('po') +subdir('protocol') +subdir('src') +subdir('plugins') + +run_data = configuration_data() +run_data.set('ABS_BUILDDIR', meson.current_build_dir()) +run_data.set('ABS_SRCDIR', meson.current_source_dir()) +run_data.set('PLUGIN_SCHEMA_PATH', plugin_schema_path) +configure_file(input: 'run.in', output: 'run', configuration: run_data) + +subdir('searchd') +subdir('tests') +subdir('tools') +subdir('docs') +subdir('calendar-server') + +summary( + { + 'Tests': get_option('tests'), + 'Phoc tests': run_phoc_tests, + 'Documentation': get_option('gtk_doc'), + 'Introspection': enable_introspection, + 'Manual pages': get_option('man'), + 'Tools': get_option('tools'), + 'Lockscreen Plugins': get_option('lockscreen-plugins'), + 'Quick Setting Plugins': get_option('quick-setting-plugins'), + 'Animation slowdown': get_option('animation-slowdown'), + 'Bindings Library': bindings_lib, + 'ABI Compliance Check': abi_check, + 'Searchd': get_option('searchd'), + }, + bool_yn: true, + section: 'Build', +) + +summary( + {'Compositor:': get_option('compositor')}, + bool_yn: true, + section: 'Runtime', +) + +gnome.post_install(glib_compile_schemas: true, gtk_update_icon_cache: true) diff --git a/meson_options.txt b/meson_options.txt new file mode 100644 index 000000000..9beb8f04d --- /dev/null +++ b/meson_options.txt @@ -0,0 +1,61 @@ +option('tests', + type: 'boolean', value: true, + description: 'Whether to compile unit tests') + +option('phoc_tests', + type: 'feature', value: 'auto', + description: 'Whether to enable tests that need phoc') + +option('gtk_doc', + type: 'boolean', value: false, + description: 'Whether to generate the API reference for Phosh') + +option('man', + type: 'boolean', value : false, + description : 'generate man pages (requires rst2man)') + +option('compositor', + type: 'string', value: '/usr/bin/phoc', + description: 'Path to the Phoc compositor for use in the launcher script') + +option('callui-i18n', + type: 'boolean', value: false, + description: 'Whether to install libcallui\s i18n fles') + +option('animation-slowdown', + type: 'integer', value: 1, + description: 'Slowdown for phosh specific animations') + +# Tools helping with e.g. notification server development +option('tools', + type: 'boolean', value: false, + description: 'Whether to build the tools') + +option('lockscreen-plugins', + type: 'boolean', value: true, + description: 'Whether to build the lockscreen plugins') + +option('quick-setting-plugins', + type: 'boolean', value: true, + description: 'Whether to build the quick setting plugins') + +option('introspection', + type: 'boolean', value: false, + description: 'Build gobject-introspection support') + +option('vapi', + type: 'boolean', value: false, + description: 'Build vala VAPI files') + +# Install files needed to generate some bindings (e.g. Rust). +option('bindings-lib', + type: 'boolean', value: false, + description: 'Whether to install the headers and shared library to generate bindings.') + +option('abi-check', + type: 'boolean', value: false, + description: 'Runs abi-compliance-checker on libphosh.so') + +option('searchd', + type: 'boolean', value: false, + description: 'Enable build and installation of phosh\'s search service') diff --git a/phosh.doap b/phosh.doap new file mode 100644 index 000000000..78578d3c2 --- /dev/null +++ b/phosh.doap @@ -0,0 +1,33 @@ + + + + phosh + phosh + A pure Wayland shell for mobile devices + Phosh aims to be a graphical shell for mobile devices + running Linux like the Librem 5. + + + + C + + + + Guido Günther + + guidog + + + + + Arun Mani J + + arun-mani-j + + + + diff --git a/plugins/caffeine-quick-setting/caffeine-quick-setting.c b/plugins/caffeine-quick-setting/caffeine-quick-setting.c new file mode 100644 index 000000000..fea45648d --- /dev/null +++ b/plugins/caffeine-quick-setting/caffeine-quick-setting.c @@ -0,0 +1,412 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "caffeine-quick-setting.h" +#include "interval-row.h" +#include "plugin-shell.h" +#include "status-page.h" + +#include + +#include + +#define CAFFEINE_QUICK_SETTING_SCHEMA "mobi.phosh.plugins.caffeine-quick-setting" +#define CAFFEINE_INTERVALS_KEY "intervals" +#define CAFFEINE_SELECTED_KEY "selected-index" + +#define UPDATE_INTERVAL 1 /* seconds */ +#define CAFFEINE_ON_ICON "cafe-hot-symbolic" +#define CAFFEINE_OFF_ICON "cafe-cold-symbolic" + +/** + * PhoshCaffeineQuickSetting: + * + * A quick setting to keep the session from going idle + */ + +enum { + PROP_0, + PROP_INHIBITED, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +struct _PhoshCaffeineQuickSetting { + PhoshQuickSetting parent; + + PhoshStatusPage *status_page; + PhoshStatusIcon *info; + guint cookie; + + GtkStack *stack; + GtkListBox *listbox; + GtkListBoxRow *cur_row; + GSettings *settings; + + uint remaining; + uint update_id; +}; + +G_DEFINE_TYPE (PhoshCaffeineQuickSetting, phosh_caffeine_quick_setting, PHOSH_TYPE_QUICK_SETTING); + +static void +phosh_caffeine_quick_setting_inhibit (PhoshCaffeineQuickSetting *self, gboolean inhibit) +{ + PhoshSessionManager *manager = phosh_shell_get_session_manager (phosh_shell_get_default ()); + + if (inhibit == !!self->cookie) + return; + + if (inhibit) { + self->cookie = phosh_session_manager_inhibit (manager, + PHOSH_SESSION_INHIBIT_IDLE | + PHOSH_SESSION_INHIBIT_SUSPEND, + /* Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled */ + _("Phosh on caffeine")); + } else { + phosh_session_manager_uninhibit (manager, self->cookie); + self->cookie = 0; + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INHIBITED]); +} + + +static void +phosh_caffeine_quick_setting_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCaffeineQuickSetting *self = PHOSH_CAFFEINE_QUICK_SETTING (object); + + switch (property_id) { + case PROP_INHIBITED: + phosh_caffeine_quick_setting_inhibit (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_caffeine_quick_setting_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCaffeineQuickSetting *self = PHOSH_CAFFEINE_QUICK_SETTING (object); + + switch (property_id) { + case PROP_INHIBITED: + g_value_set_boolean (value, !!self->cookie); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_caffeine_quick_setting_clear_timer (PhoshCaffeineQuickSetting *self) +{ + self->remaining = 0; + + g_clear_handle_id (&self->update_id, g_source_remove); + + phosh_caffeine_quick_setting_inhibit (self, FALSE); +} + + +static gboolean +on_update_expired (gpointer user_data) +{ + PhoshCaffeineQuickSetting *self = PHOSH_CAFFEINE_QUICK_SETTING (user_data); + + g_return_val_if_fail (self->remaining >= UPDATE_INTERVAL, FALSE); + self->remaining -= UPDATE_INTERVAL; + if (self->remaining > 0) { + g_autofree char *label = cui_call_format_duration ((double) self->remaining); + phosh_status_icon_set_info (self->info, label); + + return G_SOURCE_CONTINUE; + } + + phosh_caffeine_quick_setting_clear_timer (self); + + return G_SOURCE_REMOVE; +} + + +static void +on_clicked (PhoshCaffeineQuickSetting *self) +{ + uint value; + + if (self->cur_row) + value = phosh_interval_row_get_value (PHOSH_INTERVAL_ROW (self->cur_row)); + else + value = PHOSH_INTERVAL_ROW_INFINITY_VALUE; /* No interval, default to infinity */ + + if (value >= PHOSH_INTERVAL_ROW_INFINITY_VALUE) { + phosh_caffeine_quick_setting_inhibit (self, !self->cookie); + + return; + } + + /* Clear timer if on */ + if (self->update_id) { + phosh_caffeine_quick_setting_clear_timer (self); + + return; + } + + self->remaining = value; + self->update_id = g_timeout_add_seconds (UPDATE_INTERVAL, on_update_expired, self); + phosh_caffeine_quick_setting_inhibit (self, TRUE); +} + + +static void +phosh_caffeine_quick_setting_set_selected_row (PhoshCaffeineQuickSetting *self, + GtkListBoxRow *row) +{ + if (self->cur_row) + phosh_interval_row_set_selected (PHOSH_INTERVAL_ROW (self->cur_row), FALSE); + + self->cur_row = row; + phosh_interval_row_set_selected (PHOSH_INTERVAL_ROW (self->cur_row), TRUE); +} + + +static void +on_interval_row_activated (GtkListBox *listbox, + GtkListBoxRow *row, + PhoshCaffeineQuickSetting *self) +{ + uint selected_idx = 0; + g_autoptr (GList) children = NULL; + + if (self->cur_row != row) { + phosh_caffeine_quick_setting_clear_timer (self); + + children = gtk_container_get_children (GTK_CONTAINER (self->listbox)); + for (GList *child = children; child; child = child->next) { + if (child->data == row) + break; + + selected_idx++; + } + + g_settings_set_uint (self->settings, CAFFEINE_SELECTED_KEY, selected_idx); + /* Setting selected-index will update row selection, but + * do it here too to start the timer */ + phosh_caffeine_quick_setting_set_selected_row (self, row); + on_clicked (self); + } + + g_signal_emit_by_name (self->status_page, "done", TRUE); +} + + +static gboolean +transform_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean inhibited = g_value_get_boolean (from_value); + const char *icon_name; + + icon_name = inhibited ? CAFFEINE_ON_ICON : CAFFEINE_OFF_ICON; + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static gboolean +transform_to_label (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshCaffeineQuickSetting *self = PHOSH_CAFFEINE_QUICK_SETTING (user_data); + gboolean inhibited = g_value_get_boolean (from_value); + + if (!inhibited) { + g_value_set_string (to_value, C_("caffeine-disabled", "Off")); + } else if (self->update_id) { + g_autofree char *label = cui_call_format_duration ((uint) self->remaining); + g_value_set_string (to_value, label); + } else { + g_value_set_string (to_value, C_("caffeine-enabled", "On")); + } + + return TRUE; +} + + +static void +phosh_caffeine_quick_setting_finalize (GObject *gobject) +{ + PhoshCaffeineQuickSetting *self = PHOSH_CAFFEINE_QUICK_SETTING (gobject); + + if (self->cookie) + phosh_caffeine_quick_setting_inhibit (self, FALSE); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_caffeine_quick_setting_parent_class)->finalize (gobject); +} + + +static void +phosh_caffeine_quick_setting_class_init (PhoshCaffeineQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_caffeine_quick_setting_finalize; + object_class->set_property = phosh_caffeine_quick_setting_set_property; + object_class->get_property = phosh_caffeine_quick_setting_get_property; + + props[PROP_INHIBITED] = + g_param_spec_boolean ("inhibited", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY |G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/caffeine-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSetting, info); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSetting, status_page); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSetting, listbox); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSetting, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_interval_row_activated); +} + + +static int +sort_rows_func (GtkListBoxRow *r1, GtkListBoxRow *r2, gpointer user_data) +{ + uint v1 = phosh_interval_row_get_value (PHOSH_INTERVAL_ROW (r1)); + uint v2 = phosh_interval_row_get_value (PHOSH_INTERVAL_ROW (r2)); + + /* Overflow is possible as values are unsigned, + * so we compare them manually. */ + if (v1 == v2) + return 0; + + return v1 < v2 ? -1 : 1; +} + + +static void +on_selected_index_changed (PhoshCaffeineQuickSetting *self) +{ + uint selected_idx = g_settings_get_uint (self->settings, CAFFEINE_SELECTED_KEY); + g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->listbox)); + uint len = g_list_length (children); + + phosh_caffeine_quick_setting_clear_timer (self); + + /* Possible that we don't have any intervals */ + if (len) { + GtkListBoxRow *row; + + /* If index is out of bounds, select the last in the list */ + if (selected_idx >= len) { + g_debug ("Invalid interval index, defaulting to last interval"); + + selected_idx = len - 1; + } + + row = gtk_list_box_get_row_at_index (self->listbox, selected_idx); + phosh_caffeine_quick_setting_set_selected_row (self, row); + } +} + + +static void +on_intervals_changed (PhoshCaffeineQuickSetting *self) +{ + uint interval; + GVariantIter iter; + PhoshIntervalRow* row = NULL; + g_autoptr (GVariant) intervals = NULL; + g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->listbox)); + + g_debug ("Intervals changed, reconfiguring listbox..."); + + self->cur_row = NULL; + + for (GList *child = children; child; child = child->next) + gtk_container_remove (GTK_CONTAINER (self->listbox), child->data); + + intervals = g_settings_get_value (self->settings, CAFFEINE_INTERVALS_KEY); + + g_variant_iter_init (&iter, intervals); + while (g_variant_iter_next (&iter, "u", &interval)) { + row = phosh_interval_row_new (interval, FALSE); + gtk_list_box_insert (self->listbox, GTK_WIDGET (row), -1); + } + + on_selected_index_changed (self); + gtk_stack_set_visible_child_name (self->stack, + g_variant_n_children (intervals) ? "listbox" : "empty-state"); +} + + +static void +phosh_caffeine_quick_setting_init (PhoshCaffeineQuickSetting *self) { + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/caffeine-quick-setting/icons"); + + self->settings = g_settings_new (CAFFEINE_QUICK_SETTING_SCHEMA); + + gtk_list_box_set_sort_func (self->listbox, sort_rows_func, NULL, NULL); + + g_signal_connect_object (self->settings, + "changed::" CAFFEINE_INTERVALS_KEY, + G_CALLBACK (on_intervals_changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->settings, + "changed::" CAFFEINE_SELECTED_KEY, + G_CALLBACK (on_selected_index_changed), + self, + G_CONNECT_SWAPPED); + + g_object_bind_property (self, "inhibited", + self, "active", + G_BINDING_DEFAULT | + G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self, "inhibited", + self->info, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_icon_name, + NULL, NULL, NULL); + + g_object_bind_property_full (self, "inhibited", + self->info, "info", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_label, + NULL, self, NULL); + + on_intervals_changed (self); +} diff --git a/plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in b/plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in new file mode 100644 index 000000000..4ff3a41a2 --- /dev/null +++ b/plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in @@ -0,0 +1,12 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Caffeine Quick Setting +Types=quick-setting; +Comment=Prevent the session from going idle +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=caffeine-quick-setting + +[Prefs] +Id=@name@-prefs +Plugin=@plugin_prefs_dir@/libphosh-plugin-prefs-@name@.so diff --git a/plugins/caffeine-quick-setting/caffeine-quick-setting.h b/plugins/caffeine-quick-setting/caffeine-quick-setting.h new file mode 100644 index 000000000..f220f4016 --- /dev/null +++ b/plugins/caffeine-quick-setting/caffeine-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CAFFEINE_QUICK_SETTING phosh_caffeine_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshCaffeineQuickSetting, + phosh_caffeine_quick_setting, + PHOSH, CAFFEINE_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/caffeine-quick-setting/icons/cafe-cold-symbolic.svg b/plugins/caffeine-quick-setting/icons/cafe-cold-symbolic.svg new file mode 100644 index 000000000..16ec6a6dd --- /dev/null +++ b/plugins/caffeine-quick-setting/icons/cafe-cold-symbolic.svg @@ -0,0 +1,137 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/plugins/caffeine-quick-setting/icons/cafe-hot-symbolic.svg b/plugins/caffeine-quick-setting/icons/cafe-hot-symbolic.svg new file mode 100644 index 000000000..59a2f8460 --- /dev/null +++ b/plugins/caffeine-quick-setting/icons/cafe-hot-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/plugins/caffeine-quick-setting/interval-row.c b/plugins/caffeine-quick-setting/interval-row.c new file mode 100644 index 000000000..191a33096 --- /dev/null +++ b/plugins/caffeine-quick-setting/interval-row.c @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Rudra Pratap Singh + */ + +#include "interval-row.h" + +#include + +#include + +/** + * PhoshIntervalRow: + * + * A widget to display timed intervals + */ +enum { + PROP_0, + PROP_VALUE, + PROP_SELECTED, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshIntervalRow { + HdyActionRow parent; + + GtkRevealer *revealer; + uint value; + gboolean selected; +}; + +G_DEFINE_TYPE (PhoshIntervalRow, phosh_interval_row, HDY_TYPE_ACTION_ROW); + +static void +phosh_interval_row_set_value (PhoshIntervalRow *self, uint value) +{ + self->value = value; + + if (value < PHOSH_INTERVAL_ROW_INFINITY_VALUE) { + g_autofree char *label = cui_call_format_duration ((double) value); + + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), label); + } else { + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), "∞"); + } + + g_object_bind_property (self, + "selected", + self->revealer, + "reveal-child", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +} + + +static void +phosh_interval_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshIntervalRow *self = PHOSH_INTERVAL_ROW (object); + + switch (property_id) { + case PROP_VALUE: + phosh_interval_row_set_value (self, g_value_get_uint (value)); + break; + case PROP_SELECTED: + phosh_interval_row_set_selected (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_interval_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshIntervalRow *self = PHOSH_INTERVAL_ROW (object); + + switch (property_id) { + case PROP_SELECTED: + g_value_set_boolean (value, self->selected); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_interval_row_class_init (PhoshIntervalRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_interval_row_get_property; + object_class->set_property = phosh_interval_row_set_property; + + /** + * PhoshIntervalRow:value: + * + * The value of interval, in seconds + */ + props[PROP_VALUE] = + g_param_spec_uint ("value", "", "", + 0, G_MAXUINT, 0, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshIntervalRow:selected: + * + * Whether this row is selected or not + */ + props[PROP_SELECTED] = + g_param_spec_boolean ("selected", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/" + "caffeine-quick-setting/interval-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshIntervalRow, revealer); +} + + +static void +phosh_interval_row_init (PhoshIntervalRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshIntervalRow * +phosh_interval_row_new (uint value, gboolean selected) +{ + return g_object_new (PHOSH_TYPE_INTERVAL_ROW, + "value", value, + "selected", selected, + NULL); +} + + +uint +phosh_interval_row_get_value (PhoshIntervalRow *self) +{ + g_return_val_if_fail (PHOSH_IS_INTERVAL_ROW (self), 0); + + return self->value; +} + + +void +phosh_interval_row_set_selected (PhoshIntervalRow *self, gboolean selected) +{ + g_return_if_fail (PHOSH_IS_INTERVAL_ROW (self)); + + if (self->selected == selected) + return; + + self->selected = selected; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED]); +} diff --git a/plugins/caffeine-quick-setting/interval-row.h b/plugins/caffeine-quick-setting/interval-row.h new file mode 100644 index 000000000..2c11b6a75 --- /dev/null +++ b/plugins/caffeine-quick-setting/interval-row.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#define PHOSH_INTERVAL_ROW_INFINITY_VALUE G_MAXUINT32 + +G_BEGIN_DECLS + +#define PHOSH_TYPE_INTERVAL_ROW (phosh_interval_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshIntervalRow, phosh_interval_row, PHOSH, INTERVAL_ROW, HdyActionRow) + +PhoshIntervalRow *phosh_interval_row_new (uint value, gboolean selected); +uint phosh_interval_row_get_value (PhoshIntervalRow *self); +void phosh_interval_row_set_selected (PhoshIntervalRow *self, gboolean selected); + +G_END_DECLS diff --git a/plugins/caffeine-quick-setting/interval-row.ui b/plugins/caffeine-quick-setting/interval-row.ui new file mode 100644 index 000000000..df286c0fa --- /dev/null +++ b/plugins/caffeine-quick-setting/interval-row.ui @@ -0,0 +1,27 @@ + + + + + + diff --git a/plugins/caffeine-quick-setting/meson.build b/plugins/caffeine-quick-setting/meson.build new file mode 100644 index 000000000..40c628bce --- /dev/null +++ b/plugins/caffeine-quick-setting/meson.build @@ -0,0 +1,68 @@ +name = 'caffeine-quick-setting' + +caffeine_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-caffeine-quick-setting-resources', + 'phosh-plugin-caffeine-quick-setting.gresources.xml', + c_name: 'phosh_plugin_caffeine_quick_setting', +) + +caffeine_quick_setting_plugin_sources = files( + 'caffeine-quick-setting.c', + 'interval-row.c', + 'phosh-plugin-caffeine-quick-setting.c', +) + +phosh_caffeine_quick_setting_plugin = shared_module( + 'phosh-plugin-caffeine-quick-setting', + caffeine_quick_setting_plugin_sources, + caffeine_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: [plugin_dep, libcall_ui_dep], + include_directories: phosh_inc, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/cafe-hot-symbolic.svg', + rename: 'caffeine-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, +) + +caffeine_quick_setting_schema = 'mobi.phosh.plugins.caffeine-quick-setting.gschema.xml' +compiled = gnome.compile_schemas(depend_files: caffeine_quick_setting_schema) +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test( + 'Validate @0@ schema file'.format(caffeine_quick_setting_schema), + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()], + ) +endif +install_data( + caffeine_quick_setting_schema, + install_dir: 'share/glib-2.0/schemas', +) + +subdir('prefs') diff --git a/plugins/caffeine-quick-setting/mobi.phosh.plugins.caffeine-quick-setting.gschema.xml b/plugins/caffeine-quick-setting/mobi.phosh.plugins.caffeine-quick-setting.gschema.xml new file mode 100644 index 000000000..ebcbbc2da --- /dev/null +++ b/plugins/caffeine-quick-setting/mobi.phosh.plugins.caffeine-quick-setting.gschema.xml @@ -0,0 +1,22 @@ + + + + + [120, 300, 900, 4294967295] + Time intervals for which caffeine can remain on + + Various time interval options, during which caffeine will stay on, in seconds. + + + + + 3 + Index of selected interval + + Corresponding index of the selected interval. If out of bounds, + default value of infinity is selected. + + + + diff --git a/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.c b/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.c new file mode 100644 index 000000000..e71172fe9 --- /dev/null +++ b/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "caffeine-quick-setting.h" +#include "phosh-plugin.h" + + +char **g_io_phosh_plugin_caffeine_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_CAFFEINE_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_caffeine_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.gresources.xml b/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.gresources.xml new file mode 100644 index 000000000..baec381dc --- /dev/null +++ b/plugins/caffeine-quick-setting/phosh-plugin-caffeine-quick-setting.gresources.xml @@ -0,0 +1,9 @@ + + + + qs.ui + interval-row.ui + icons/cafe-hot-symbolic.svg + icons/cafe-cold-symbolic.svg + + diff --git a/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c b/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c new file mode 100644 index 000000000..2a30ad950 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c @@ -0,0 +1,306 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Rudra Pratap Singh + */ + +#include "phosh-plugin-prefs-config.h" + +#include "caffeine-quick-setting-prefs.h" + +#include + +#include + +#define INFINITY_VALUE G_MAXUINT32 +#define CAFFEINE_QUICK_SETTING_SCHEMA "mobi.phosh.plugins.caffeine-quick-setting" +#define CAFFEINE_INTERVALS_KEY "intervals" + +struct _PhoshCaffeineQuickSettingPrefs { + AdwPreferencesDialog parent; + + GtkStack *stack; + GtkListBox *listbox; + GtkSpinButton *hours_btn; + GtkSpinButton *minutes_btn; + GtkSpinButton *seconds_btn; + AdwDialog *add_interval_dialog; + GSimpleActionGroup *action_group; + GSettings *settings; +}; + +G_DEFINE_TYPE (PhoshCaffeineQuickSettingPrefs, + phosh_caffeine_quick_setting_prefs, + ADW_TYPE_PREFERENCES_DIALOG); + +static void +phosh_caffeine_quick_setting_prefs_finalize (GObject *object) +{ + PhoshCaffeineQuickSettingPrefs *self = PHOSH_CAFFEINE_QUICK_SETTING_PREFS (object); + + g_clear_object (&self->settings); + g_clear_object (&self->action_group); + + G_OBJECT_CLASS (phosh_caffeine_quick_setting_prefs_parent_class)->finalize (object); +} + + +static GVariantBuilder * +rebuild_intervals_without_row (PhoshCaffeineQuickSettingPrefs *self, + GtkListBoxRow *remove_row) +{ + uint i = 0; + GtkListBoxRow *row; + GVariantBuilder *builder = g_variant_builder_new (G_VARIANT_TYPE ("au")); + + while ((row = gtk_list_box_get_row_at_index (self->listbox, i++))) { + uint value; + + if (row == remove_row) + continue; + + value = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (row), "value")); + g_variant_builder_add (builder, "u", value); + } + + return builder; +} + + +static void +clear_values (PhoshCaffeineQuickSettingPrefs *self) +{ + gtk_spin_button_set_value (self->hours_btn, 0); + gtk_spin_button_set_value (self->minutes_btn, 0); + gtk_spin_button_set_value (self->seconds_btn, 0); +} + + +static void +on_quickstart_interval_clicked (GSimpleAction *action, GVariant *param, gpointer user_data) +{ + PhoshCaffeineQuickSettingPrefs *self = PHOSH_CAFFEINE_QUICK_SETTING_PREFS (user_data); + uint value = g_variant_get_uint32 (param); + g_autoptr (GVariantBuilder) builder = rebuild_intervals_without_row (self, NULL); + + g_variant_builder_add (builder, "u", value); + g_settings_set_value (self->settings, CAFFEINE_INTERVALS_KEY, g_variant_builder_end (builder)); + + adw_dialog_close (self->add_interval_dialog); +} + + +static void +on_add_interval_added (PhoshCaffeineQuickSettingPrefs *self, + GtkButton *button) +{ + uint remaining = 0; + uint hours = (uint) gtk_spin_button_get_value (self->hours_btn); + uint minutes_to_s = (uint) gtk_spin_button_get_value (self->minutes_btn) * 60; + uint seconds = (uint) gtk_spin_button_get_value (self->seconds_btn); + g_autoptr (GVariantBuilder) builder = NULL; + + /* Just watch out for hours, as it can potentially overflow */ + if (hours < INFINITY_VALUE / 3600 + INFINITY_VALUE % 3600) { + uint t = INFINITY_VALUE - hours * 3600; + + if (t > minutes_to_s) { + t -= minutes_to_s; + if (t > seconds) + remaining = t - seconds; + } + } + + builder = rebuild_intervals_without_row (self, NULL); + g_variant_builder_add (builder, "u", INFINITY_VALUE - remaining); + g_settings_set_value (self->settings, CAFFEINE_INTERVALS_KEY, g_variant_builder_end (builder)); + + adw_dialog_close (self->add_interval_dialog); +} + + +static void +on_add_interval_cancelled (PhoshCaffeineQuickSettingPrefs *self) +{ + clear_values (self); + adw_dialog_close (self->add_interval_dialog); +} + + +static void +on_add_interval_clicked (PhoshCaffeineQuickSettingPrefs *self, + GtkButton *button) +{ + adw_dialog_present (self->add_interval_dialog, GTK_WIDGET (self)); +} + + +static void +phosh_caffeine_quick_setting_prefs_class_init (PhoshCaffeineQuickSettingPrefsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_caffeine_quick_setting_prefs_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/" + "caffeine-quick-setting-prefs/prefs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, stack); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, listbox); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, add_interval_dialog); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, hours_btn); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, minutes_btn); + gtk_widget_class_bind_template_child (widget_class, PhoshCaffeineQuickSettingPrefs, seconds_btn); + + gtk_widget_class_bind_template_callback (widget_class, on_add_interval_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_add_interval_cancelled); + gtk_widget_class_bind_template_callback (widget_class, on_add_interval_added); +} + + +static int +sort_rows_func (GtkListBoxRow *r1, GtkListBoxRow *r2, gpointer user_data) +{ + uint v1 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (r1), "value")); + uint v2 = GPOINTER_TO_UINT (g_object_get_data (G_OBJECT (r2), "value")); + + /* Overflow is possible as values are unsigned, + * so we compare them manually. */ + if (v1 == v2) + return 0; + + return v1 < v2 ? -1 : 1; +} + + +typedef struct { + PhoshCaffeineQuickSettingPrefs *self; + AdwActionRow *row; /* Row to which the button is suffixed */ +} RemoveButtonData; + +static void +remove_button_data_free (RemoveButtonData *data) +{ + g_object_unref (data->self); + g_free (data); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RemoveButtonData, remove_button_data_free); + + +static void +on_remove_btn_clicked (gpointer user_data) +{ + g_autoptr (RemoveButtonData) data = user_data; + g_autoptr (GVariantBuilder) builder = NULL; + + g_assert (data); + + builder = rebuild_intervals_without_row (data->self, GTK_LIST_BOX_ROW (data->row)); + g_settings_set_value (data->self->settings, + CAFFEINE_INTERVALS_KEY, + g_variant_builder_end (builder)); +} + + +static GtkWidget * +create_remove_button (AdwActionRow *row) +{ + GtkWidget *remove_btn = gtk_button_new_from_icon_name ("list-remove-symbolic"); + + gtk_widget_add_css_class (remove_btn, "flat"); + gtk_widget_set_hexpand (remove_btn, TRUE); + gtk_widget_set_halign (remove_btn, GTK_ALIGN_END); + + return remove_btn; +} + + +static void +on_intervals_changed (PhoshCaffeineQuickSettingPrefs *self) +{ + uint interval; + GVariantIter iter; + AdwActionRow *row; + g_autoptr (GVariant) intervals = NULL; + + gtk_list_box_remove_all (self->listbox); + intervals = g_settings_get_value (self->settings, CAFFEINE_INTERVALS_KEY); + + g_variant_iter_init (&iter, intervals); + while (g_variant_iter_next (&iter, "u", &interval)) { + GtkWidget *remove_btn; + RemoveButtonData *remove_btn_data; + + row = ADW_ACTION_ROW (adw_action_row_new ()); + remove_btn = create_remove_button (row); + adw_action_row_add_suffix (ADW_ACTION_ROW (row), remove_btn); + + remove_btn_data = g_new0 (RemoveButtonData, 1); + remove_btn_data->self = g_object_ref (self); + remove_btn_data->row = row; + + g_object_set_data (G_OBJECT (row), "value", GUINT_TO_POINTER (interval)); + + if (interval < INFINITY_VALUE) { + g_autofree char *title = cui_call_format_duration (interval); + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), title); + } else { + adw_preferences_row_set_title (ADW_PREFERENCES_ROW (row), _("No timeout (∞)")); + } + + g_signal_connect_swapped (remove_btn, + "clicked", + G_CALLBACK (on_remove_btn_clicked), + remove_btn_data); + + gtk_list_box_insert (self->listbox, GTK_WIDGET (row), -1); + } + + gtk_stack_set_visible_child_name (self->stack, + g_variant_n_children (intervals) ? "listbox" : "empty-state"); +} + + +static GActionEntry entries[] = +{ + { .name = "update-value", .activate = on_quickstart_interval_clicked, .parameter_type = "u" }, +}; + +static void +phosh_caffeine_quick_setting_prefs_init (PhoshCaffeineQuickSettingPrefs *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (CAFFEINE_QUICK_SETTING_SCHEMA); + gtk_list_box_set_sort_func (self->listbox, sort_rows_func, NULL, NULL); + g_signal_connect_object (self->settings, + "changed::" CAFFEINE_INTERVALS_KEY, + G_CALLBACK (on_intervals_changed), + self, + G_CONNECT_SWAPPED); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/caffeine-quick-setting-prefs/stylesheet/common.css"); + gtk_style_context_add_provider_for_display (gdk_display_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + self->action_group = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->action_group), + entries, + G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self->add_interval_dialog), + "interval", + G_ACTION_GROUP (self->action_group)); + + on_intervals_changed (self); +} diff --git a/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.h b/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.h new file mode 100644 index 000000000..3bc760533 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CAFFEINE_QUICK_SETTING_PREFS (phosh_caffeine_quick_setting_prefs_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshCaffeineQuickSettingPrefs, + phosh_caffeine_quick_setting_prefs, + PHOSH, CAFFEINE_QUICK_SETTING_PREFS, + AdwPreferencesDialog) + +G_END_DECLS diff --git a/plugins/caffeine-quick-setting/prefs/meson.build b/plugins/caffeine-quick-setting/prefs/meson.build new file mode 100644 index 000000000..aa3871057 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/meson.build @@ -0,0 +1,25 @@ +caffeine_quick_setting_prefs_resources = gnome.compile_resources( + 'phosh-plugin-prefs-caffeine-quick-setting-resources', + 'phosh-plugin-prefs-caffeine-quick-setting.gresources.xml', + c_name: 'phosh_plugin_prefs_caffeine_quick_setting', +) + +caffeine_quick_setting_plugin_prefs_sources = files( + 'caffeine-quick-setting-prefs.c', + 'caffeine-quick-setting-prefs.h', + 'phosh-plugin-prefs-caffeine-quick-setting.c', +) + +phosh_caffeine_quick_setting_plugin_prefs = shared_module( + 'phosh-plugin-prefs-caffeine-quick-setting', + caffeine_quick_setting_plugin_prefs_sources, + caffeine_quick_setting_prefs_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-prefs-caffeine-quick-setting"', + '-DPLUGIN_PREFS_NAME="@0@-prefs"'.format(name), + '-DADW_VERSION_MIN_REQUIRED=@0@'.format(adw_ver_str), + ], + dependencies: [plugin_prefs_dep, libcall_ui_dep], + install: true, + install_dir: plugin_prefs_dir, +) diff --git a/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.c b/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.c new file mode 100644 index 000000000..cdd032e9a --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.c @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Rudra Pratap Singh + */ + +#include "phosh-plugin-prefs-config.h" + +#include "caffeine-quick-setting-prefs.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_prefs_caffeine_quick_setting_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS, + PHOSH_TYPE_CAFFEINE_QUICK_SETTING_PREFS, + PLUGIN_PREFS_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_prefs_caffeine_quick_setting_query (void) +{ + char *extension_points[] = { PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS, NULL }; + + return g_strdupv (extension_points); +} diff --git a/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.gresources.xml b/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.gresources.xml new file mode 100644 index 000000000..4ceb879f6 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/phosh-plugin-prefs-caffeine-quick-setting.gresources.xml @@ -0,0 +1,7 @@ + + + + prefs.ui + stylesheet/common.css + + diff --git a/plugins/caffeine-quick-setting/prefs/prefs.ui b/plugins/caffeine-quick-setting/prefs/prefs.ui new file mode 100644 index 000000000..c7d65beb8 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/prefs.ui @@ -0,0 +1,292 @@ + + + + + + + Add New Interval + + + + + 0 + 0 + + + _Cancel + 1 + center + + + + + + _Add + 1 + center + + + + + + + + + 300 + true + true + center + + + 20 + 20 + 20 + 20 + center + center + vertical + 6 + + + 10 + 10 + center + Quickstart Intervals + + + + + 20 + center + center + 14 + 14 + + + 5 m + interval.update-value + uint32 300 + + 0 + 0 + + + + + + 15 m + interval.update-value + uint32 900 + + 0 + 1 + + + + + + 30 m + interval.update-value + uint32 1800 + + 0 + 2 + + + + + + 1 h + interval.update-value + uint32 3600 + + 0 + 3 + + + + + + No timeout (∞) + interval.update-value + uint32 4294967295 + + 1 + 0 + 4 + + + + + + + + 10 + 10 + center + Choose Interval + + + + + center + center + 6 + + + vertical + 3 + 1 + 1 + + + 4294967295 + 1 + + + + + 0 + 0 + + + + + + : + + + 0 + 1 + + + + + + vertical + 3 + 1 + 1 + + + 59 + 1 + + + + + 0 + 2 + + + + + + : + + + 0 + 3 + + + + + + vertical + 3 + 1 + 1 + + + 59 + 1 + + + + + 0 + 4 + + + + + + + + + + + + + diff --git a/plugins/caffeine-quick-setting/prefs/stylesheet/common.css b/plugins/caffeine-quick-setting/prefs/stylesheet/common.css new file mode 100644 index 000000000..a1486b5a8 --- /dev/null +++ b/plugins/caffeine-quick-setting/prefs/stylesheet/common.css @@ -0,0 +1,11 @@ +.caffeine-spinbutton { + font-size: 200%; +} + +.caffeine-spinbutton > text { + padding: 10px; +} + +.caffeine-spinbutton-separator { + font-size: 400%; +} diff --git a/plugins/caffeine-quick-setting/qs.ui b/plugins/caffeine-quick-setting/qs.ui new file mode 100644 index 000000000..1c28c5c1c --- /dev/null +++ b/plugins/caffeine-quick-setting/qs.ui @@ -0,0 +1,59 @@ + + + + + + 1 + 16 + + + 1 + Caffeine timers + stack + footer + + + 1 + + + 1 + none + + + + + listbox + + + + + 1 + cafe-hot-symbolic + No caffeine intervals + + + empty-state + + + + + 1 + 1 + panel.launch-mobile-panel + ("topbar", [<"caffeine-quick-setting">]) + 0 + + + 1 + end + Caffeine Settings + + + + diff --git a/plugins/calendar/calendar.c b/plugins/calendar/calendar.c new file mode 100644 index 000000000..d82f883ae --- /dev/null +++ b/plugins/calendar/calendar.c @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "calendar.h" + +/** + * PhoshCalendar + * + * Display the calenadar. + */ +struct _PhoshCalendar { + GtkBox parent; + + GtkCalendar *calendar; + GSettings *calendar_settings; +}; + +G_DEFINE_TYPE (PhoshCalendar, phosh_calendar, GTK_TYPE_BOX); + + +static void +phosh_calendar_dispose (GObject *object) +{ + PhoshCalendar *self = PHOSH_CALENDAR (object); + + g_clear_object (&self->calendar_settings); + + G_OBJECT_CLASS (phosh_calendar_parent_class)->dispose (object); +} + + +static void +on_settings_changed (GSettings *settings, + const char *key, + gpointer user_data) +{ + PhoshCalendar *self = PHOSH_CALENDAR (user_data); + GtkCalendarDisplayOptions display_options; + gboolean display_week; + + display_options = gtk_calendar_get_display_options (self->calendar); + display_week = g_settings_get_boolean (settings, key); + + if (display_week) + display_options |= GTK_CALENDAR_SHOW_WEEK_NUMBERS; + else + display_options &= ~GTK_CALENDAR_SHOW_WEEK_NUMBERS; + + gtk_calendar_set_display_options (self->calendar, display_options); +} + + +static void +phosh_calendar_class_init (PhoshCalendarClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_calendar_dispose; + + g_type_ensure (GTK_TYPE_CALENDAR); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/calendar/calendar.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshCalendar, calendar); + + gtk_widget_class_set_css_name (widget_class, "phosh-calendar"); +} + +static void +phosh_calendar_init (PhoshCalendar *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/calendar/stylesheet/common.css"); + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self)), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + self->calendar_settings = g_settings_new ("org.gnome.desktop.calendar"); + g_signal_connect (self->calendar_settings, + "changed::show-weekdate", + G_CALLBACK (on_settings_changed), + self); + on_settings_changed (self->calendar_settings, "show-weekdate", self); +} diff --git a/plugins/calendar/calendar.desktop.in.in b/plugins/calendar/calendar.desktop.in.in new file mode 100644 index 000000000..b97eb1ac7 --- /dev/null +++ b/plugins/calendar/calendar.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Calendar +Types=lockscreen; +Comment=A simple calendar widget +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +NoDisplay=true \ No newline at end of file diff --git a/plugins/calendar/calendar.h b/plugins/calendar/calendar.h new file mode 100644 index 000000000..bc4279bff --- /dev/null +++ b/plugins/calendar/calendar.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CALENDAR (phosh_calendar_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshCalendar, phosh_calendar, PHOSH, CALENDAR, GtkBox) + +G_END_DECLS diff --git a/plugins/calendar/calendar.ui b/plugins/calendar/calendar.ui new file mode 100644 index 000000000..86baedf6d --- /dev/null +++ b/plugins/calendar/calendar.ui @@ -0,0 +1,16 @@ + + + + + diff --git a/plugins/calendar/meson.build b/plugins/calendar/meson.build new file mode 100644 index 000000000..5bd97b10d --- /dev/null +++ b/plugins/calendar/meson.build @@ -0,0 +1,39 @@ +name = 'calendar' + +calendar_resources = gnome.compile_resources( + 'phosh-plugin-calendar-resources', + 'phosh-plugin-calendar.gresources.xml', + c_name: 'phosh_plugin_calendar', +) + +calendar_plugin_sources = files('calendar.c', 'phosh-plugin-calendar.c') + +phosh_calendar_plugin = shared_module( + 'phosh-plugin-calendar', + calendar_plugin_sources, + calendar_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) diff --git a/plugins/calendar/phosh-plugin-calendar.c b/plugins/calendar/phosh-plugin-calendar.c new file mode 100644 index 000000000..856967b64 --- /dev/null +++ b/plugins/calendar/phosh-plugin-calendar.c @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "calendar.h" +#include "phosh-plugin.h" + +#include +#include + +char **g_io_phosh_plugin_calendar_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_CALENDAR, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + +char ** +g_io_phosh_plugin_calendar_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/calendar/phosh-plugin-calendar.gresources.xml b/plugins/calendar/phosh-plugin-calendar.gresources.xml new file mode 100644 index 000000000..228c347b7 --- /dev/null +++ b/plugins/calendar/phosh-plugin-calendar.gresources.xml @@ -0,0 +1,7 @@ + + + + calendar.ui + stylesheet/common.css + + diff --git a/plugins/calendar/stylesheet/common.css b/plugins/calendar/stylesheet/common.css new file mode 100644 index 000000000..c8e04a8fd --- /dev/null +++ b/plugins/calendar/stylesheet/common.css @@ -0,0 +1,5 @@ +phosh-calendar { + background-color: @phosh_notification_bg_color; + border-radius: 12px; + padding: 8px; +} diff --git a/plugins/dark-mode-quick-setting/dark-mode-quick-setting.c b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.c new file mode 100644 index 000000000..26096ca7a --- /dev/null +++ b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.c @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Teemu Ikonen + */ + +#include "dark-mode-quick-setting.h" + +#include +#include + +/** + * PhoshDarkModeQuickSetting: + * + * Set the color-scheme enum. + */ + +struct _PhoshDarkModeQuickSetting { + PhoshQuickSetting parent; + + GSettings *settings; + PhoshStatusIcon *info; +}; + +G_DEFINE_TYPE (PhoshDarkModeQuickSetting, phosh_dark_mode_quick_setting, PHOSH_TYPE_QUICK_SETTING); + + +static const char* +enum_to_info (int color_scheme) +{ + const char *labels[] = { + _("Default style"), + _("Dark mode"), + _("Light mode"), + }; + return labels[CLAMP (color_scheme, 0, 2)]; +} + + +static const char* +enum_to_icon_name (int color_scheme) +{ + const char *icons[] = { + "dark-mode-disabled-symbolic", + "dark-mode-symbolic", + "weather-clear", + }; + return icons[CLAMP (color_scheme, 0, 2)]; +} + + +static void +set_props_from_enum (PhoshDarkModeQuickSetting *self, + int color_scheme) +{ + g_object_set (self->info, "icon-name", enum_to_icon_name (color_scheme), NULL); + g_object_set (self->info, "info", enum_to_info (color_scheme), NULL); + g_object_set (self, "active", color_scheme == 1, NULL); +} + + +static void +on_clicked (PhoshDarkModeQuickSetting *self) +{ + int color_scheme = g_settings_get_enum (self->settings, "color-scheme"); + + /* Only allow setting 'default' or 'prefer-dark' by clicking */ + g_settings_set_enum (self->settings, "color-scheme", + (color_scheme == G_DESKTOP_COLOR_SCHEME_PREFER_DARK) ? + G_DESKTOP_COLOR_SCHEME_DEFAULT : + G_DESKTOP_COLOR_SCHEME_PREFER_DARK); +} + + +static void +on_color_scheme_changed (PhoshDarkModeQuickSetting *self, + char *_key, + GSettings *_settings) +{ + int color_scheme = g_settings_get_enum (self->settings, "color-scheme"); + + set_props_from_enum (self, color_scheme); +} + + +static void +phosh_lockscreen_finalize (GObject *object) +{ + PhoshDarkModeQuickSetting *self = PHOSH_DARK_MODE_QUICK_SETTING (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_dark_mode_quick_setting_parent_class)->finalize (object); +} + + +static void +phosh_dark_mode_quick_setting_class_init (PhoshDarkModeQuickSettingClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_lockscreen_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/dark-mode-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshDarkModeQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_dark_mode_quick_setting_init (PhoshDarkModeQuickSetting *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/dark-mode-quick-setting/icons"); + + self->settings = g_settings_new ("org.gnome.desktop.interface"); + + /* Initialize label and icon */ + on_color_scheme_changed (self, "color-scheme", self->settings); + + g_signal_connect_swapped (self->settings, "changed::color-scheme", + (GCallback) on_color_scheme_changed, self); +} diff --git a/plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in new file mode 100644 index 000000000..e56e04a0a --- /dev/null +++ b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Dark Mode / Color Scheme Quick Setting +Types=quick-setting; +Comment=Toggle dark mode +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=dark-mode-quick-setting diff --git a/plugins/dark-mode-quick-setting/dark-mode-quick-setting.h b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.h new file mode 100644 index 000000000..62f7c7e05 --- /dev/null +++ b/plugins/dark-mode-quick-setting/dark-mode-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Teemu Ikonen + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DARK_MODE_QUICK_SETTING phosh_dark_mode_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshDarkModeQuickSetting, + phosh_dark_mode_quick_setting, + PHOSH, DARK_MODE_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/dark-mode-quick-setting/icons/dark-mode-disabled-symbolic.svg b/plugins/dark-mode-quick-setting/icons/dark-mode-disabled-symbolic.svg new file mode 100644 index 000000000..4f96e548c --- /dev/null +++ b/plugins/dark-mode-quick-setting/icons/dark-mode-disabled-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/dark-mode-quick-setting/icons/dark-mode-symbolic.svg b/plugins/dark-mode-quick-setting/icons/dark-mode-symbolic.svg new file mode 100644 index 000000000..d0ebc7c8d --- /dev/null +++ b/plugins/dark-mode-quick-setting/icons/dark-mode-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/dark-mode-quick-setting/meson.build b/plugins/dark-mode-quick-setting/meson.build new file mode 100644 index 000000000..7c11d7bf4 --- /dev/null +++ b/plugins/dark-mode-quick-setting/meson.build @@ -0,0 +1,52 @@ +name = 'dark-mode-quick-setting' + +dark_mode_quick_setting_deps = [ + dependency('gsettings-desktop-schemas', version: '>=42'), +] + +dark_mode_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-dark-mode-quick-setting-resources', + 'phosh-plugin-dark-mode-quick-setting.gresources.xml', + c_name: 'phosh_plugin_dark_mode_quick_setting', +) + +dark_mode_quick_setting_plugin_sources = files( + 'dark-mode-quick-setting.c', + 'phosh-plugin-dark-mode-quick-setting.c', +) + +phosh_dark_mode_quick_setting_plugin = shared_module( + 'phosh-plugin-dark-mode-quick-setting', + dark_mode_quick_setting_plugin_sources, + dark_mode_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: [plugin_dep, dark_mode_quick_setting_deps], + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/dark-mode-symbolic.svg', + rename: 'dark-mode-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, +) diff --git a/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.c b/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.c new file mode 100644 index 000000000..629002545 --- /dev/null +++ b/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Teemu Ikonen + */ + +#include "dark-mode-quick-setting.h" +#include "phosh-plugin.h" + + +char **g_io_phosh_plugin_dark_mode_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_DARK_MODE_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_dark_mode_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.gresources.xml b/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.gresources.xml new file mode 100644 index 000000000..cfb1f7cd2 --- /dev/null +++ b/plugins/dark-mode-quick-setting/phosh-plugin-dark-mode-quick-setting.gresources.xml @@ -0,0 +1,8 @@ + + + + qs.ui + icons/dark-mode-symbolic.svg + icons/dark-mode-disabled-symbolic.svg + + diff --git a/plugins/dark-mode-quick-setting/qs.ui b/plugins/dark-mode-quick-setting/qs.ui new file mode 100644 index 000000000..c1421993a --- /dev/null +++ b/plugins/dark-mode-quick-setting/qs.ui @@ -0,0 +1,12 @@ + + + + + + 1 + 16 + + diff --git a/plugins/emergency-info/emergency-info-common.h b/plugins/emergency-info/emergency-info-common.h new file mode 100644 index 000000000..eddac7179 --- /dev/null +++ b/plugins/emergency-info/emergency-info-common.h @@ -0,0 +1,12 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#define EMERGENCY_INFO_GKEYFILE_LOCATION "phosh" +#define EMERGENCY_INFO_GKEYFILE_NAME "EmergencyInfo.keyfile" +#define INFO_GROUP "Info" +#define CONTACTS_GROUP "Contacts" diff --git a/plugins/emergency-info/emergency-info-row.c b/plugins/emergency-info/emergency-info-row.c new file mode 100644 index 000000000..e4ab5d42a --- /dev/null +++ b/plugins/emergency-info/emergency-info-row.c @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + +#include "emergency-info-row.h" + +#include +#include + +enum { + PROP_0, + PROP_NUMBER, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshEmergencyInfoRow { + HdyActionRow parent; + + GtkLabel *label_contact; +}; + +G_DEFINE_TYPE (PhoshEmergencyInfoRow, phosh_emergency_info_row, HDY_TYPE_ACTION_ROW) + +/* TODO: Clicking on the emergency contact does nothing. */ + +static void +phosh_emergency_info_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyInfoRow *self = PHOSH_EMERGENCY_INFO_ROW (object); + + switch (property_id) { + case PROP_NUMBER: + gtk_label_set_text (self->label_contact, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_emergency_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyInfoRow *self = PHOSH_EMERGENCY_INFO_ROW (object); + + switch (property_id) { + case PROP_NUMBER: + g_value_set_string (value, gtk_label_get_text (self->label_contact)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_emergency_info_row_class_init (PhoshEmergencyInfoRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_emergency_info_get_property; + object_class->set_property = phosh_emergency_info_set_property; + + /** + * PhoshEmergencyInfoRow::number + * + * The phone number + */ + props[PROP_NUMBER] = + g_param_spec_string ("number", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/emergency-info/emergency-info-row.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoRow, label_contact); +} + +static void +phosh_emergency_info_row_init (PhoshEmergencyInfoRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +phosh_emergency_info_row_new (const char *contact, + const char *number, + const char *relationship) +{ + PhoshEmergencyInfoRow *self; + + self = g_object_new (PHOSH_TYPE_EMERGENCY_INFO_ROW, + "title", contact, + "subtitle", relationship, + "number", number, + NULL); + + return GTK_WIDGET (self); +} diff --git a/plugins/emergency-info/emergency-info-row.h b/plugins/emergency-info/emergency-info-row.h new file mode 100644 index 000000000..50e7745c7 --- /dev/null +++ b/plugins/emergency-info/emergency-info-row.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "emergency-info.h" + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_INFO_ROW (phosh_emergency_info_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyInfoRow, phosh_emergency_info_row, PHOSH, EMERGENCY_INFO_ROW, HdyActionRow) + +GtkWidget *phosh_emergency_info_row_new (const char *contact, + const char *number, + const char *relationship); + +G_END_DECLS diff --git a/plugins/emergency-info/emergency-info-row.ui b/plugins/emergency-info/emergency-info-row.ui new file mode 100644 index 000000000..b658ddbae --- /dev/null +++ b/plugins/emergency-info/emergency-info-row.ui @@ -0,0 +1,15 @@ + + + + + + diff --git a/plugins/emergency-info/emergency-info.c b/plugins/emergency-info/emergency-info.c new file mode 100644 index 000000000..58fb1193d --- /dev/null +++ b/plugins/emergency-info/emergency-info.c @@ -0,0 +1,328 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + +#include "emergency-info.h" +#include "emergency-info-row.h" +#include "emergency-info-common.h" + +/** + * PhoshEmergencyInfo: + * + * Show emergency information and contacts + * Calling contacts is not supported + * For now, you need to manually create a GKeyfile at + * `~/.config/phosh/EmergencyInfo.keyfile` with contents looking like: + * ``` + * [Info] + * # Emergency Info On the Owner + * # If you do not add one of the items (or leave the item blank), + * # the section will not display in the lockscreen. + * + * OwnerName=Chris + * # You can make a newline with \n + * HomeAddress=Lorem ipsum\nLorem ipsum + * DateOfBirth=1 Jan, 1970 + * PreferredLanguage=C, MATLAB + * Age=10,000 + * BloodType=Lorem ipsum + * Height=163 cm + * Weight=74 kg + * # You can add multiple and separate by a ";" + * Allergies=Lorem ipsum dolor 1;Lorem ipsum dolor 2 + * # You can add multiple and separate by a ";" + * MedicationsAndConditions=Lorem ipsum 1;Lorem ipsum 2 + * OtherInfo=Lorem ipsum dolor sit amet + * + * [Contacts] + * # You can add as many contacts as you want here + * Contact 1=(123) 555-12;Brother + * Contact 2=(204) 555-00 + * ``` + */ +struct _PhoshEmergencyInfo { + GtkBox parent; + + char *owner_name; + char *dob; + char *language; + char *home_address; + char *age; + char *blood_type; + char *height; + char *weight; + char *allergies; + char *medications_conditions; + char *other_info; + GStrv contacts; + + GtkLabel *label_owner_name; + GtkLabel *label_dob; + GtkLabel *label_language; + GtkLabel *label_age; + GtkLabel *label_blood_type; + GtkLabel *label_height; + GtkLabel *label_weight; + + HdyActionRow *row_owner_name; + HdyActionRow *row_dob; + HdyActionRow *row_language; + HdyActionRow *row_home_address; + HdyActionRow *row_age; + HdyActionRow *row_blood_type; + HdyActionRow *row_height; + HdyActionRow *row_weight; + HdyActionRow *row_allergies; + HdyActionRow *row_medications; + HdyActionRow *row_other_info; + + HdyPreferencesGroup *pers_info; + HdyPreferencesGroup *emer_info; + HdyPreferencesGroup *emer_contacts; +}; + +G_DEFINE_TYPE (PhoshEmergencyInfo, phosh_emergency_info, GTK_TYPE_BOX); + +static gboolean +set_subtitle_or_hide_widget (const char *label, + HdyActionRow *widget_action_row) +{ + gboolean visible; + + visible = !!(label && *label); + hdy_action_row_set_subtitle (widget_action_row, label); + gtk_widget_set_visible (GTK_WIDGET (widget_action_row), visible); + return visible; +} + +static gboolean +set_label_or_hide_widget (const char *label, + GtkLabel *widget_label, + GtkWidget *widget) +{ + gboolean visible; + + visible = !!(label && *label); + gtk_label_set_text (widget_label, label); + gtk_widget_set_visible (widget, visible); + return visible; +} + +static void +load_info (PhoshEmergencyInfo *self) +{ + g_autofree char *path = NULL; + g_autoptr(GError) err = NULL; + g_auto (GStrv) temp_med_cond = NULL; + g_auto (GStrv) temp_allergies = NULL; + g_autoptr (GKeyFile) key_file = NULL; + gboolean display_med_info = FALSE; + gboolean display_pers_info = FALSE; + gsize i; + + path = g_build_filename (g_get_user_config_dir (), + EMERGENCY_INFO_GKEYFILE_LOCATION, + EMERGENCY_INFO_GKEYFILE_NAME, + NULL); + + key_file = g_key_file_new (); + + if (!g_key_file_load_from_file (key_file, path, G_KEY_FILE_NONE, &err)) { + g_warning ("Failed to load keyfile at '%s': %s", path, err->message); + return; + } + + self->owner_name = g_key_file_get_string (key_file, + INFO_GROUP, + "OwnerName", + NULL); + + self->dob = g_key_file_get_string (key_file, + INFO_GROUP, + "DateOfBirth", + NULL); + + self->language = g_key_file_get_string (key_file, + INFO_GROUP, + "PreferredLanguage", + NULL); + + self->home_address = g_key_file_get_string (key_file, + INFO_GROUP, + "HomeAddress", + NULL); + + self->age = g_key_file_get_string (key_file, + INFO_GROUP, + "Age", + NULL); + + self->blood_type = g_key_file_get_string (key_file, + INFO_GROUP, + "BloodType", + NULL); + + self->height = g_key_file_get_string (key_file, + INFO_GROUP, + "Height", + NULL); + + self->weight = g_key_file_get_string (key_file, + INFO_GROUP, + "Weight", + NULL); + + temp_allergies = g_key_file_get_string_list (key_file, + INFO_GROUP, + "Allergies", + NULL, + NULL); + + if (temp_allergies) + self->allergies = g_strjoinv ("\n", temp_allergies); + + temp_med_cond = g_key_file_get_string_list (key_file, + INFO_GROUP, + "MedicationsAndConditions", + NULL, + NULL); + + if (temp_med_cond) + self->medications_conditions = g_strjoinv ("\n", temp_med_cond); + + self->other_info = g_key_file_get_string (key_file, + INFO_GROUP, + "OtherInfo", + NULL); + + self->contacts = g_key_file_get_keys (key_file, + CONTACTS_GROUP, + NULL, + NULL); + + for (i = 0; self->contacts && self->contacts[i]; i++) { + g_autofree char *number = NULL; + GtkWidget *new_row; + + number = g_key_file_get_string (key_file, + CONTACTS_GROUP, + self->contacts[i], NULL); + if (number && *number) { + g_auto (GStrv) number_split = NULL; + + number_split = g_strsplit (number, ";", 2); + new_row = phosh_emergency_info_row_new (self->contacts[i], + number_split[0], + number_split[1]); + gtk_container_add (GTK_CONTAINER (self->emer_contacts), + GTK_WIDGET (new_row)); + } + } + + if (!self->contacts || !self->contacts[0]) + gtk_widget_set_visible (GTK_WIDGET (self->emer_contacts), FALSE); + + set_label_or_hide_widget (self->owner_name, self->label_owner_name, + GTK_WIDGET (self->row_owner_name)); + display_pers_info |= set_label_or_hide_widget (self->dob, self->label_dob, + GTK_WIDGET (self->row_dob)); + display_pers_info |= set_label_or_hide_widget (self->language, self->label_language, + GTK_WIDGET (self->row_language)); + display_pers_info |= set_subtitle_or_hide_widget (self->home_address, + self->row_home_address); + display_med_info |= set_label_or_hide_widget (self->age, self->label_age, + GTK_WIDGET (self->row_age)); + display_med_info |= set_label_or_hide_widget (self->blood_type, self->label_blood_type, + GTK_WIDGET (self->row_blood_type)); + display_med_info |= set_label_or_hide_widget (self->height, self->label_height, + GTK_WIDGET (self->row_height)); + display_med_info |= set_label_or_hide_widget (self->weight, self->label_weight, + GTK_WIDGET (self->row_weight)); + display_med_info |= set_subtitle_or_hide_widget (self->allergies, + self->row_allergies); + display_med_info |= set_subtitle_or_hide_widget (self->medications_conditions, + self->row_medications); + display_med_info |= set_subtitle_or_hide_widget (self->other_info, + self->row_other_info); + gtk_widget_set_visible (GTK_WIDGET (self->emer_info), display_med_info); + gtk_widget_set_visible (GTK_WIDGET (self->pers_info), display_pers_info); +} + +static void +phosh_emergency_info_finalize (GObject *object) +{ + PhoshEmergencyInfo *self = PHOSH_EMERGENCY_INFO (object); + + g_free (self->owner_name); + g_free (self->dob); + g_free (self->language); + g_free (self->home_address); + g_free (self->age); + g_free (self->blood_type); + g_free (self->height); + g_free (self->weight); + g_free (self->allergies); + g_free (self->other_info); + g_free (self->medications_conditions); + g_strfreev (self->contacts); + + G_OBJECT_CLASS (phosh_emergency_info_parent_class)->finalize (object); +} + +static void +phosh_emergency_info_class_init (PhoshEmergencyInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_emergency_info_finalize; + + g_type_ensure (PHOSH_TYPE_EMERGENCY_INFO_ROW); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/emergency-info/emergency-info.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_owner_name); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_dob); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_language); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_age); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_blood_type); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_height); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, label_weight); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_owner_name); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_home_address); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_dob); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_language); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_allergies); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_medications); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_other_info); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_age); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_blood_type); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_height); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, row_weight); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, pers_info); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, emer_info); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfo, emer_contacts); + + gtk_widget_class_set_css_name (widget_class, "phosh-emergency-info"); +} + +static void +phosh_emergency_info_init (PhoshEmergencyInfo *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/emergency-info/stylesheet/common.css"); + + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self)), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + load_info (self); +} diff --git a/plugins/emergency-info/emergency-info.desktop.in.in b/plugins/emergency-info/emergency-info.desktop.in.in new file mode 100644 index 000000000..df2c4de0b --- /dev/null +++ b/plugins/emergency-info/emergency-info.desktop.in.in @@ -0,0 +1,11 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Emergency Info +Types=lockscreen; +Comment=Show emergency information and contacts +Plugin=@plugins_dir@/libphosh-plugin-@name@.so + +[Prefs] +Id=@name@-prefs +Plugin=@plugin_prefs_dir@/libphosh-plugin-prefs-@name@.so diff --git a/plugins/emergency-info/emergency-info.h b/plugins/emergency-info/emergency-info.h new file mode 100644 index 000000000..74092dcf9 --- /dev/null +++ b/plugins/emergency-info/emergency-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_INFO (phosh_emergency_info_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshEmergencyInfo, phosh_emergency_info, PHOSH, EMERGENCY_INFO, GtkBox) + +G_END_DECLS diff --git a/plugins/emergency-info/emergency-info.ui b/plugins/emergency-info/emergency-info.ui new file mode 100644 index 000000000..af7fe095e --- /dev/null +++ b/plugins/emergency-info/emergency-info.ui @@ -0,0 +1,221 @@ + + + + + + diff --git a/plugins/emergency-info/meson.build b/plugins/emergency-info/meson.build new file mode 100644 index 000000000..fe19932fe --- /dev/null +++ b/plugins/emergency-info/meson.build @@ -0,0 +1,51 @@ +name = 'emergency-info' + +emergency_info_plugin_deps = [plugin_dep] + +emergency_info_plugin_sources = files( + 'emergency-info-common.h', + 'emergency-info-row.c', + 'emergency-info.c', + 'phosh-plugin-emergency-info.c', +) + +emergency_info_inc = include_directories('.') + +emergency_info_resources = gnome.compile_resources( + 'phosh-plugin-emergency-info-resources', + 'phosh-plugin-emergency-info.gresources.xml', + c_name: 'phosh_plugin_emergency_info', +) + +phosh_emergency_info_plugin = shared_module( + 'phosh-plugin-emergency-info', + emergency_info_plugin_sources, + emergency_info_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: emergency_info_plugin_deps, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +subdir('prefs') diff --git a/plugins/emergency-info/phosh-plugin-emergency-info.c b/plugins/emergency-info/phosh-plugin-emergency-info.c new file mode 100644 index 000000000..e4e347c47 --- /dev/null +++ b/plugins/emergency-info/phosh-plugin-emergency-info.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + +#define G_LOG_DOMAIN "phosh-plugin-emergency-info" + +#include "phosh-plugin.h" +#include "emergency-info.h" + +char **g_io_phosh_plugin_emergency_info_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_EMERGENCY_INFO, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + +char ** +g_io_phosh_plugin_emergency_info_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/emergency-info/phosh-plugin-emergency-info.gresources.xml b/plugins/emergency-info/phosh-plugin-emergency-info.gresources.xml new file mode 100644 index 000000000..c124775e8 --- /dev/null +++ b/plugins/emergency-info/phosh-plugin-emergency-info.gresources.xml @@ -0,0 +1,8 @@ + + + + emergency-info.ui + emergency-info-row.ui + stylesheet/common.css + + diff --git a/plugins/emergency-info/prefs/emergency-info-prefs-row.c b/plugins/emergency-info/prefs/emergency-info-prefs-row.c new file mode 100644 index 000000000..2a813609f --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs-row.c @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + +#include "emergency-info-common.h" +#include "emergency-info-prefs-row.h" + +enum { + PROP_0, + PROP_NUMBER, + PROP_LAST_PROP, +}; + +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshEmergencyInfoPrefsRow { + AdwActionRow parent; + + GtkLabel *label_contact; +}; + +G_DEFINE_TYPE (PhoshEmergencyInfoPrefsRow, phosh_emergency_info_prefs_row, ADW_TYPE_ACTION_ROW) + +static void +on_delete_button_clicked (PhoshEmergencyInfoPrefsRow *self) +{ + GtkWidget *parent; + g_autofree char *path = NULL; + g_autoptr (GKeyFile) key_file = NULL; + + /* To dispose: You need to get the parent and have the parent remove it. */ + parent = gtk_widget_get_parent (GTK_WIDGET (self)); + gtk_list_box_remove (GTK_LIST_BOX (parent), (GTK_WIDGET (self))); + + path = g_build_filename (g_get_user_config_dir (), + EMERGENCY_INFO_GKEYFILE_LOCATION, + EMERGENCY_INFO_GKEYFILE_NAME, + NULL); + + key_file = g_key_file_new (); + + if (!g_key_file_load_from_file (key_file, path, G_KEY_FILE_KEEP_COMMENTS, NULL)) { + g_warning ("No Keyfile found at %s", path); + return; + } + + g_key_file_remove_key (key_file, + CONTACTS_GROUP, + adw_preferences_row_get_title (ADW_PREFERENCES_ROW (self)), + NULL); + + if (!g_key_file_save_to_file (key_file, path, NULL)) + g_warning ("Error Saving Keyfile at %s", path); +} + +static void +phosh_emergency_info_pref_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyInfoPrefsRow *self = PHOSH_EMERGENCY_INFO_PREFS_ROW (object); + + switch (property_id) { + case PROP_NUMBER: + gtk_label_set_text (self->label_contact, g_value_get_string(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_emergency_info_pref_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyInfoPrefsRow *self = PHOSH_EMERGENCY_INFO_PREFS_ROW (object); + + switch (property_id) { + case PROP_NUMBER: + g_value_set_string (value, gtk_label_get_text (self->label_contact)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_emergency_info_prefs_row_class_init (PhoshEmergencyInfoPrefsRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_emergency_info_pref_get_property; + object_class->set_property = phosh_emergency_info_pref_set_property; + + props[PROP_NUMBER] = + g_param_spec_string("number", "", "", " ", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/emergency-info-prefs/emergency-info-prefs-row.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefsRow, label_contact); + + gtk_widget_class_bind_template_callback (widget_class, on_delete_button_clicked); +} + +static void +phosh_emergency_info_prefs_row_init (PhoshEmergencyInfoPrefsRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +phosh_emergency_info_prefs_row_new (const char *contact, + const char *number, + const char *relationship) +{ + PhoshEmergencyInfoPrefsRow *self; + + self = g_object_new (PHOSH_TYPE_EMERGENCY_INFO_PREFS_ROW, + "title", contact, + "subtitle", relationship ?: "", + "number", number, + NULL); + + return GTK_WIDGET (self); +} diff --git a/plugins/emergency-info/prefs/emergency-info-prefs-row.h b/plugins/emergency-info/prefs/emergency-info-prefs-row.h new file mode 100644 index 000000000..c161d2a45 --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs-row.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_INFO_PREFS_ROW (phosh_emergency_info_prefs_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyInfoPrefsRow, phosh_emergency_info_prefs_row, PHOSH, EMERGENCY_INFO_PREFS_ROW, AdwActionRow) + +GtkWidget *phosh_emergency_info_prefs_row_new (const char *contact, + const char *number, + const char *relationship); + +G_END_DECLS diff --git a/plugins/emergency-info/prefs/emergency-info-prefs-row.ui b/plugins/emergency-info/prefs/emergency-info-prefs-row.ui new file mode 100644 index 000000000..8656135ea --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs-row.ui @@ -0,0 +1,23 @@ + + + + + diff --git a/plugins/emergency-info/prefs/emergency-info-prefs.c b/plugins/emergency-info/prefs/emergency-info-prefs.c new file mode 100644 index 000000000..9532a5c87 --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs.c @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Chris Talbot + */ + +#include "emergency-info-common.h" +#include "emergency-info-prefs.h" +#include "emergency-info-prefs-row.h" + +#define STR_IS_NULL_OR_EMPTY(x) ((x) == NULL || (x)[0] == '\0') + +/** + * PhoshEmergencyInfoPrefs + * + * Preferences for emergency-info plugin + */ +struct _PhoshEmergencyInfoPrefs { + AdwPreferencesDialog parent; + + char *owner_name; + char *dob; + char *language; + char *home_address; + char *age; + char *blood_type; + char *height; + char *weight; + char *allergies; + char *medications_conditions; + char *other_info; + GStrv contacts; + + GtkTextBuffer *home_addr_text_buffer; + GtkTextBuffer *allergies_text_buffer; + GtkTextBuffer *med_cond_text_buffer; + GtkTextBuffer *other_info_text_buffer; + + AdwEntryRow *owner_name_entry; + AdwEntryRow *dob_entry; + AdwEntryRow *pref_language_entry; + + AdwEntryRow *age_entry; + AdwEntryRow *blood_type_entry; + AdwEntryRow *height_entry; + AdwEntryRow *weight_entry; + + AdwEntryRow *contact_name_entry; + AdwEntryRow *relationship_entry; + AdwEntryRow *contact_number_entry; + + AdwDialog *add_emer_contact_dialog; + + AdwPreferencesGroup *emer_contacts; + + char *keyfile_path; +}; + +G_DEFINE_TYPE (PhoshEmergencyInfoPrefs, phosh_emergency_info_prefs, ADW_TYPE_PREFERENCES_DIALOG); + + +static void +save_keyfile (PhoshEmergencyInfoPrefs *self, GKeyFile *key_file) +{ + g_autoptr (GError) err = NULL; + g_autofree char *dirname = g_path_get_dirname (self->keyfile_path); + + g_mkdir_with_parents (dirname, 0700); + if (!g_key_file_save_to_file (key_file, self->keyfile_path, &err)) + g_warning ("Error Saving Keyfile at %s: %s", self->keyfile_path, err->message); +} + + +static void +set_or_remove_info_group (GKeyFile *key_file, + const char *key, + const char *value) +{ + + if (!!(value && *value)) + g_key_file_set_string (key_file, + INFO_GROUP, + key, + value); + else + g_key_file_remove_key (key_file, + INFO_GROUP, + key, NULL); + +} + +static void +save_settings (PhoshEmergencyInfoPrefs *self) +{ + g_autoptr (GKeyFile) key_file = g_key_file_new (); + + g_key_file_load_from_file (key_file, self->keyfile_path, G_KEY_FILE_KEEP_COMMENTS, NULL); + + set_or_remove_info_group (key_file, + "OwnerName", + self->owner_name); + + set_or_remove_info_group (key_file, + "DateOfBirth", + self->dob); + + set_or_remove_info_group (key_file, + "PreferredLanguage", + self->language); + + set_or_remove_info_group (key_file, + "HomeAddress", + self->home_address); + + + set_or_remove_info_group (key_file, + "Age", + self->age); + + set_or_remove_info_group (key_file, + "BloodType", + self->blood_type); + + set_or_remove_info_group (key_file, + "Height", + self->height); + + set_or_remove_info_group (key_file, + "Weight", + self->weight); + + if (self->allergies && !STR_IS_NULL_OR_EMPTY (self->allergies)) { + g_auto (GStrv) temp_allergies = NULL; + temp_allergies = g_strsplit (self->allergies, "\n", -1); + g_key_file_set_string_list (key_file, + INFO_GROUP, + "Allergies", + (const char * const*) temp_allergies, + g_strv_length (temp_allergies)); + } else { + g_key_file_remove_key (key_file, + INFO_GROUP, + "Allergies", NULL); + } + + if (self->medications_conditions && !STR_IS_NULL_OR_EMPTY (self->medications_conditions)) { + g_auto (GStrv) temp_med_cond = NULL; + temp_med_cond = g_strsplit (self->medications_conditions, "\n", -1); + g_key_file_set_string_list (key_file, + INFO_GROUP, + "MedicationsAndConditions", + (const char * const*)temp_med_cond, + g_strv_length (temp_med_cond)); + } else { + g_key_file_remove_key (key_file, + INFO_GROUP, + "MedicationsAndConditions", NULL); + } + + set_or_remove_info_group (key_file, + "OtherInfo", + self->other_info); + + save_keyfile (self, key_file); +} + +static void +add_contact_row (PhoshEmergencyInfoPrefs *self, + const char *contact, + const char *number) +{ + GtkWidget *new_row; + g_auto (GStrv) number_split = NULL; + + number_split = g_strsplit (number, ";", 2); + new_row = phosh_emergency_info_prefs_row_new (contact, + number_split[0], + number_split[1]); + + adw_preferences_group_add (self->emer_contacts, GTK_WIDGET (new_row)); +} + +static void +load_settings (PhoshEmergencyInfoPrefs *self) +{ + g_auto (GStrv) temp_med_cond = NULL; + g_auto (GStrv) temp_allergies = NULL; + g_autoptr (GKeyFile) key_file = g_key_file_new (); + gsize i; + + if (!g_key_file_load_from_file (key_file, self->keyfile_path, 0, NULL)) + return; + + self->owner_name = g_key_file_get_string (key_file, + INFO_GROUP, + "OwnerName", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->owner_name_entry), self->owner_name ?: ""); + + self->dob = g_key_file_get_string (key_file, + INFO_GROUP, + "DateOfBirth", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->dob_entry), self->dob ?: ""); + + self->language = g_key_file_get_string (key_file, + INFO_GROUP, + "PreferredLanguage", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->pref_language_entry), self->language ?: ""); + + self->home_address = g_key_file_get_string (key_file, + INFO_GROUP, + "HomeAddress", + NULL); + + gtk_text_buffer_set_text (self->home_addr_text_buffer, + self->home_address ?: "", + -1); + + + self->age = g_key_file_get_string (key_file, + INFO_GROUP, + "Age", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->age_entry), self->age ?: ""); + + self->blood_type = g_key_file_get_string (key_file, + INFO_GROUP, + "BloodType", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->blood_type_entry), self->blood_type ?: ""); + + self->height = g_key_file_get_string (key_file, + INFO_GROUP, + "Height", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->height_entry), self->height ?: ""); + + self->weight = g_key_file_get_string (key_file, + INFO_GROUP, + "Weight", + NULL); + + gtk_editable_set_text (GTK_EDITABLE (self->weight_entry), self->weight ?: ""); + + temp_allergies = g_key_file_get_string_list (key_file, + INFO_GROUP, + "Allergies", + NULL, + NULL); + + if (temp_allergies) + self->allergies = g_strjoinv ("\n", temp_allergies); + + gtk_text_buffer_set_text (self->allergies_text_buffer, + self->allergies ?: "", + -1); + + + temp_med_cond = g_key_file_get_string_list (key_file, + INFO_GROUP, + "MedicationsAndConditions", + NULL, + NULL); + + if (temp_med_cond) + self->medications_conditions = g_strjoinv ("\n", temp_med_cond); + + gtk_text_buffer_set_text (self->med_cond_text_buffer, + self->medications_conditions ?: "", + -1); + + self->other_info = g_key_file_get_string (key_file, + INFO_GROUP, + "OtherInfo", + NULL); + + gtk_text_buffer_set_text (self->other_info_text_buffer, + self->other_info ?: "", + -1); + + self->contacts = g_key_file_get_keys (key_file, + CONTACTS_GROUP, + NULL, + NULL); + + for (i = 0; self->contacts && self->contacts[i]; i++) { + g_autofree char *number = NULL; + number = g_key_file_get_string (key_file, + CONTACTS_GROUP, + self->contacts[i], NULL); + if (number && *number) { + add_contact_row (self, self->contacts[i], number); + } + } +} + +static void +phosh_emergency_info_prefs_free_data (PhoshEmergencyInfoPrefs *self) +{ + g_clear_pointer (&self->owner_name, g_free); + g_clear_pointer (&self->dob, g_free); + g_clear_pointer (&self->language, g_free); + g_clear_pointer (&self->home_address, g_free); + g_clear_pointer (&self->age, g_free); + g_clear_pointer (&self->blood_type, g_free); + g_clear_pointer (&self->height, g_free); + g_clear_pointer (&self->weight, g_free); + g_clear_pointer (&self->allergies, g_free); + g_clear_pointer (&self->other_info, g_free); + g_clear_pointer (&self->medications_conditions, g_free); +} + +static void +on_dialog_update_emer_contact (PhoshEmergencyInfoPrefs *self) +{ + g_autofree char *number_joined = NULL; + const char *contact = NULL; + const char *relationship = NULL; + const char *number = NULL; + g_autoptr (GKeyFile) key_file = g_key_file_new (); + + contact = gtk_editable_get_text (GTK_EDITABLE (self->contact_name_entry)); + relationship = gtk_editable_get_text (GTK_EDITABLE (self->relationship_entry)); + number = gtk_editable_get_text (GTK_EDITABLE (self->contact_number_entry)); + + number_joined = g_strdup_printf ("%s;%s", number ?: "", relationship ?: ""); + + add_contact_row (self, contact, number_joined); + + if (!g_key_file_load_from_file (key_file, self->keyfile_path, G_KEY_FILE_KEEP_COMMENTS, NULL)) + g_warning ("No Keyfile found at %s", self->keyfile_path); + + g_key_file_set_string (key_file, + CONTACTS_GROUP, + contact, + number_joined); + + save_keyfile (self, key_file); + + gtk_editable_set_text (GTK_EDITABLE (self->contact_name_entry), ""); + gtk_editable_set_text (GTK_EDITABLE (self->relationship_entry), ""); + gtk_editable_set_text (GTK_EDITABLE (self->contact_number_entry), ""); + + adw_dialog_close (ADW_DIALOG (self->add_emer_contact_dialog)); +} + +static void +on_dialog_update_emer_contact_cancelled (PhoshEmergencyInfoPrefs *self) +{ + gtk_editable_set_text (GTK_EDITABLE (self->contact_name_entry), ""); + gtk_editable_set_text (GTK_EDITABLE (self->relationship_entry), ""); + gtk_editable_set_text (GTK_EDITABLE (self->contact_number_entry), ""); + + adw_dialog_close (ADW_DIALOG (self->add_emer_contact_dialog)); +} + +static void +on_update_emer_contact (PhoshEmergencyInfoPrefs *self) +{ + adw_dialog_present (self->add_emer_contact_dialog, GTK_WIDGET (self)); +} + +static void +on_update_information_cancelled (PhoshEmergencyInfoPrefs *self) +{ + adw_dialog_close (ADW_DIALOG (self)); +} + +static void +on_update_information_clicked (PhoshEmergencyInfoPrefs *self) +{ + GtkTextIter start, end; + + phosh_emergency_info_prefs_free_data (self); + + self->owner_name = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->owner_name_entry))); + self->dob = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->dob_entry))); + self->language = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->pref_language_entry))); + + gtk_text_buffer_get_start_iter (self->home_addr_text_buffer, &start); + gtk_text_buffer_get_end_iter (self->home_addr_text_buffer, &end); + self->home_address = gtk_text_buffer_get_text (self->home_addr_text_buffer, + &start, &end, true); + + self->age = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->age_entry))); + self->blood_type = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->blood_type_entry))); + self->height = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->height_entry))); + self->weight = g_strdup (gtk_editable_get_text (GTK_EDITABLE (self->weight_entry))); + + gtk_text_buffer_get_start_iter (self->allergies_text_buffer, &start); + gtk_text_buffer_get_end_iter (self->allergies_text_buffer, &end); + self->allergies = gtk_text_buffer_get_text (self->allergies_text_buffer, &start, &end, true); + + gtk_text_buffer_get_start_iter (self->med_cond_text_buffer, &start); + gtk_text_buffer_get_end_iter (self->med_cond_text_buffer, &end); + self->medications_conditions = gtk_text_buffer_get_text (self->med_cond_text_buffer, + &start, &end, true); + + gtk_text_buffer_get_start_iter (self->other_info_text_buffer, &start); + gtk_text_buffer_get_end_iter (self->other_info_text_buffer, &end); + self->other_info = gtk_text_buffer_get_text (self->other_info_text_buffer, + &start, &end, true); + + save_settings (self); + + adw_dialog_close (ADW_DIALOG (self)); +} + + +static void +phosh_emergency_info_prefs_finalize (GObject *object) +{ + PhoshEmergencyInfoPrefs *self = PHOSH_EMERGENCY_INFO_PREFS (object); + + phosh_emergency_info_prefs_free_data (self); + g_clear_pointer (&self->contacts, g_strfreev); + g_clear_pointer (&self->keyfile_path, g_free); + + G_OBJECT_CLASS (phosh_emergency_info_prefs_parent_class)->finalize (object); +} + + +static void +phosh_emergency_info_prefs_class_init (PhoshEmergencyInfoPrefsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_emergency_info_prefs_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/emergency-info-prefs/emergency-info-prefs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, home_addr_text_buffer); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, allergies_text_buffer); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, med_cond_text_buffer); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, other_info_text_buffer); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, owner_name_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, dob_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, pref_language_entry); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, age_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, blood_type_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, height_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, weight_entry); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, contact_name_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, relationship_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, contact_number_entry); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, add_emer_contact_dialog); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyInfoPrefs, emer_contacts); + + gtk_widget_class_bind_template_callback (widget_class, on_update_emer_contact); + gtk_widget_class_bind_template_callback (widget_class, on_update_information_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_update_information_cancelled); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_update_emer_contact); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_update_emer_contact_cancelled); +} + + +static void +phosh_emergency_info_prefs_init (PhoshEmergencyInfoPrefs *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->keyfile_path = g_build_filename (g_get_user_config_dir (), + EMERGENCY_INFO_GKEYFILE_LOCATION, + EMERGENCY_INFO_GKEYFILE_NAME, + NULL); + load_settings (self); +} diff --git a/plugins/emergency-info/prefs/emergency-info-prefs.h b/plugins/emergency-info/prefs/emergency-info-prefs.h new file mode 100644 index 000000000..2f8c1b92f --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2022 Chris Talbot + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_INFO_PREFS (phosh_emergency_info_prefs_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshEmergencyInfoPrefs, phosh_emergency_info_prefs, PHOSH, EMERGENCY_INFO_PREFS, AdwPreferencesDialog) + +G_END_DECLS diff --git a/plugins/emergency-info/prefs/emergency-info-prefs.ui b/plugins/emergency-info/prefs/emergency-info-prefs.ui new file mode 100644 index 000000000..24859f20a --- /dev/null +++ b/plugins/emergency-info/prefs/emergency-info-prefs.ui @@ -0,0 +1,263 @@ + + + + + + + + + + + Add New Contact + + + + + 0 + 0 + + + _Cancel + 1 + center + + + + + + _Add + 1 + center + + + contact_name_entry + + + + + + + + + + + + + _Contact Name + 1 + + + + + Relationship + 1 + + + + + _Contact Number + 1 + + + + + + + + + + diff --git a/plugins/emergency-info/prefs/meson.build b/plugins/emergency-info/prefs/meson.build new file mode 100644 index 000000000..1892d803b --- /dev/null +++ b/plugins/emergency-info/prefs/meson.build @@ -0,0 +1,32 @@ +emergency_info_plugin_prefs_deps = [ + dependency('gtk4'), + dependency('libadwaita-1'), +] + +emergency_info_prefs_resources = gnome.compile_resources( + 'phosh-plugin-prefs-emergency-info-resources', + 'phosh-plugin-prefs-emergency-info.gresources.xml', + c_name: 'phosh_plugin_prefs_emergency_info', +) + +emergency_info_plugin_prefs_sources = files( + 'emergency-info-prefs-row.c', + 'emergency-info-prefs-row.h', + 'emergency-info-prefs.c', + 'emergency-info-prefs.h', + 'phosh-plugin-prefs-emergency-info.c', +) + +phosh_emergency_info_plugin_prefs = shared_module( + 'phosh-plugin-prefs-emergency-info', + emergency_info_plugin_prefs_sources, + emergency_info_prefs_resources, + include_directories: [emergency_info_inc, plugin_prefs_inc], + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-prefs-emergency-info-prefs"', + '-DPLUGIN_PREFS_NAME="@0@-prefs"'.format(name), + ], + dependencies: emergency_info_plugin_prefs_deps, + install: true, + install_dir: plugin_prefs_dir, +) diff --git a/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.c b/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.c new file mode 100644 index 000000000..ea4bc9b75 --- /dev/null +++ b/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.c @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2022 Chris Talbot + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "phosh-plugin-prefs-config.h" + +#include "emergency-info-prefs.h" +#include "phosh-plugin.h" + +#include + +char **g_io_phosh_plugin_prefs_emergency_info_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, + PHOSH_TYPE_EMERGENCY_INFO_PREFS, + PLUGIN_PREFS_NAME, + 10); + + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_prefs_emergency_info_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.gresources.xml b/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.gresources.xml new file mode 100644 index 000000000..3a9375af4 --- /dev/null +++ b/plugins/emergency-info/prefs/phosh-plugin-prefs-emergency-info.gresources.xml @@ -0,0 +1,7 @@ + + + + emergency-info-prefs.ui + emergency-info-prefs-row.ui + + diff --git a/plugins/emergency-info/stylesheet/common.css b/plugins/emergency-info/stylesheet/common.css new file mode 100644 index 000000000..9f5d4e7c8 --- /dev/null +++ b/plugins/emergency-info/stylesheet/common.css @@ -0,0 +1,5 @@ +phosh-emergency-info { + background-color: @phosh_notification_bg_color; + border-radius: 12px; + padding: 0 8px +} diff --git a/plugins/launcher-box/launcher-box.c b/plugins/launcher-box/launcher-box.c new file mode 100644 index 000000000..255708efb --- /dev/null +++ b/plugins/launcher-box/launcher-box.c @@ -0,0 +1,302 @@ +/* + * Copyright (C) 2023-2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "launcher-box.h" +#include "launcher-item.h" +#include "launcher-row.h" + +#include "plugin-shell.h" +#include "launcher-entry-manager.h" + +#include + +#define LAUNCHER_BOX_SCHEMA_ID "sm.puri.phosh.plugins.launcher-box" +#define LAUNCHER_BOX_FOLDER_KEY "folder" + +#define STR_IS_NULL_OR_EMPTY(x) ((x) == NULL || (x)[0] == '\0') + +/** + * PhoshLauncherBox: + * + * Show launchers in a folder. + */ +struct _PhoshLauncherBox { + GtkBox parent; + + GFileMonitor *monitor; + GFile *dir; + char *launcher_box_path; + GCancellable *cancel; + + GListStore *model; + GtkListBox *lb_launchers; + GtkStack *stack_launchers; +}; + +G_DEFINE_TYPE (PhoshLauncherBox, phosh_launcher_box, GTK_TYPE_BOX); + +static void +on_row_selected (PhoshLauncherBox *self, + GtkListBoxRow *row, + GtkListBox *box) +{ + g_autoptr (GError) err = NULL; + PhoshLauncherItem *item; + GDesktopAppInfo *app_info; + GdkAppLaunchContext *context; + + if (row == NULL) + return; + + item = phosh_launcher_row_get_item (PHOSH_LAUNCHER_ROW (row)); + app_info = phosh_launcher_item_get_app_info (item); + g_debug ("row selected: %s", g_app_info_get_display_name (G_APP_INFO (app_info))); + + context = gdk_display_get_app_launch_context (gtk_widget_get_display (GTK_WIDGET (self))); + + g_app_info_launch (G_APP_INFO (app_info), NULL, G_APP_LAUNCH_CONTEXT (context), &err); + + gtk_list_box_select_row (box, NULL); +} + + +static void +on_view_close_clicked (PhoshLauncherBox *self) +{ + gtk_stack_set_visible_child_name (self->stack_launchers, "launchers"); +} + + +static void +phosh_launcher_box_finalize (GObject *object) +{ + PhoshLauncherBox *self = PHOSH_LAUNCHER_BOX (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->model); + + g_clear_object (&self->monitor); + g_clear_object (&self->dir); + g_clear_pointer (&self->launcher_box_path, g_free); + + G_OBJECT_CLASS (phosh_launcher_box_parent_class)->finalize (object); +} + + +static void +phosh_launcher_box_class_init (PhoshLauncherBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_launcher_box_finalize; + + g_type_ensure (PHOSH_TYPE_LAUNCHER_ROW); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/launcher-box/launcher-box.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshLauncherBox, lb_launchers); + gtk_widget_class_bind_template_child (widget_class, PhoshLauncherBox, stack_launchers); + gtk_widget_class_bind_template_callback (widget_class, on_view_close_clicked); + + gtk_widget_class_set_css_name (widget_class, "phosh-launcher-box"); +} + + +static GtkWidget * +create_launcher_row (gpointer item, gpointer user_data) +{ + return phosh_launcher_row_new (PHOSH_LAUNCHER_ITEM (item)); +} + + +static gint +launcher_item_compare (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GDesktopAppInfo *info_a = phosh_launcher_item_get_app_info ((gpointer)a); + GDesktopAppInfo *info_b = phosh_launcher_item_get_app_info ((gpointer)b); + const char *f_a = g_desktop_app_info_get_filename (info_a); + const char *f_b = g_desktop_app_info_get_filename (info_b); + + return g_strcmp0 (f_a, f_b); +} + + +static void +on_file_child_enumerated (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GError) err = NULL; + g_autoptr (GFileEnumerator) enumerator = NULL; + GFile *dir = G_FILE (source_object); + PhoshLauncherBox *self; + const char *stack_child = "launchers"; + + enumerator = g_file_enumerate_children_finish (dir, res, &err); + if (enumerator == NULL) { + g_warning ("Failed to list %s", g_file_get_path (dir)); + return; + } + + self = PHOSH_LAUNCHER_BOX (user_data); + + while (TRUE) { + g_autoptr (GDesktopAppInfo) app_info = NULL; + g_autofree char *path = NULL; + g_autoptr (PhoshLauncherItem) item = NULL; + GFile *file; + GFileInfo *info; + + if (!g_file_enumerator_iterate (enumerator, &info, &file, self->cancel, &err)) { + g_warning ("Failed to list contents of launcher dir %s: $%s", self->launcher_box_path, err->message); + return; + } + + if (!file) + break; + + if (!g_str_has_suffix (g_file_info_get_name (info), ".desktop")) + continue; + + if (g_strcmp0 (g_file_info_get_content_type (info), "application/x-desktop") != 0) + continue; + + path = g_file_get_path (file); + app_info = g_desktop_app_info_new_from_filename (path); + if (!app_info) + continue; + + item = phosh_launcher_item_new (app_info); + + g_list_store_insert_sorted (self->model, item, launcher_item_compare, NULL); + } + + if (g_list_model_get_n_items (G_LIST_MODEL (self->model)) == 0) + stack_child = "no-launchers"; + + gtk_stack_set_visible_child_name (self->stack_launchers, stack_child); +} + + +static void +load_launchers (PhoshLauncherBox *self) +{ + g_autoptr (GSettings) settings = g_settings_new (LAUNCHER_BOX_SCHEMA_ID); + g_autofree char *folder = NULL; + + folder = g_settings_get_string (settings, LAUNCHER_BOX_FOLDER_KEY); + if (STR_IS_NULL_OR_EMPTY (folder)) { + self->launcher_box_path = g_build_filename (g_get_user_config_dir (), + "phosh", "plugins", "launcher-box", NULL); + } else { + self->launcher_box_path = g_steal_pointer (&folder); + } + + self->dir = g_file_new_for_path (self->launcher_box_path); + /* + * Since the lockscreen is rebuilt on lock we don't worry about changes in directory contents, + * should we do, we can add a GFileMonitor later on + */ + g_file_enumerate_children_async (self->dir, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON "," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," + G_FILE_ATTRIBUTE_TIME_MODIFIED "," + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_LOW, + self->cancel, + on_file_child_enumerated, + self); +} + + +static void +update_item (PhoshLauncherItem *item, GVariant *properties) +{ + double progress; + gint64 count; + gboolean visible; + + if (g_variant_lookup (properties, "progress", "d", &progress)) + phosh_launcher_item_set_progress (item, progress); + + if (g_variant_lookup (properties, "progress-visible", "b", &visible)) + phosh_launcher_item_set_progress_visible (item, visible); + + if (g_variant_lookup (properties, "count", "x", &count)) + phosh_launcher_item_set_count (item, count); + + if (g_variant_lookup (properties, "count-visible", "b", &visible)) + phosh_launcher_item_set_count_visible (item, visible); +} + + +static void +on_launcher_info_updated (PhoshLauncherBox *self, char *desktop_file, GVariant *properties) +{ + g_return_if_fail (PHOSH_IS_LAUNCHER_BOX (self)); + + g_debug ("Received info for '%s'", desktop_file); + + for (int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->model)); i++) { + g_autoptr (PhoshLauncherItem) item = NULL; + GDesktopAppInfo *info; + const char *app_id; + + item = g_list_model_get_item (G_LIST_MODEL (self->model), i); + info = phosh_launcher_item_get_app_info (item); + + app_id = g_app_info_get_id (G_APP_INFO (info)); + if (g_strcmp0 (app_id, desktop_file) == 0) { + g_debug ("Update info for '%s'", desktop_file); + update_item (item, properties); + break; + } + } +} + + +static void +phosh_launcher_box_init (PhoshLauncherBox *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + PhoshShell *shell = phosh_shell_get_default (); + PhoshLauncherEntryManager *launcher_entry_manager; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->model = g_list_store_new (PHOSH_TYPE_LAUNCHER_ITEM); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/launcher-box/stylesheet/common.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_list_box_bind_model (self->lb_launchers, + G_LIST_MODEL (self->model), + create_launcher_row, + NULL, + NULL); + + g_signal_connect_swapped (self->lb_launchers, + "row-selected", + G_CALLBACK (on_row_selected), + self); + + load_launchers (self); + + launcher_entry_manager = phosh_shell_get_launcher_entry_manager (shell); + g_signal_connect_object (launcher_entry_manager, + "info-updated", + G_CALLBACK (on_launcher_info_updated), + self, + G_CONNECT_SWAPPED); +} diff --git a/plugins/launcher-box/launcher-box.desktop.in.in b/plugins/launcher-box/launcher-box.desktop.in.in new file mode 100644 index 000000000..217445e61 --- /dev/null +++ b/plugins/launcher-box/launcher-box.desktop.in.in @@ -0,0 +1,6 @@ +[Plugin] +Id=@name@ +Name=Launcher Box +Types=lockscreen; +Comment=Add launchers to the lock screen. This plugin is experimental. +Plugin=@plugins_dir@/libphosh-plugin-@name@.so diff --git a/plugins/launcher-box/launcher-box.h b/plugins/launcher-box/launcher-box.h new file mode 100644 index 000000000..ce6c6caf3 --- /dev/null +++ b/plugins/launcher-box/launcher-box.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LAUNCHER_BOX (phosh_launcher_box_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshLauncherBox, phosh_launcher_box, PHOSH, LAUNCHER_BOX, GtkBox) + +G_END_DECLS diff --git a/plugins/launcher-box/launcher-box.ui b/plugins/launcher-box/launcher-box.ui new file mode 100644 index 000000000..5c0b660cb --- /dev/null +++ b/plugins/launcher-box/launcher-box.ui @@ -0,0 +1,52 @@ + + + + + diff --git a/plugins/launcher-box/launcher-item.c b/plugins/launcher-box/launcher-item.c new file mode 100644 index 000000000..1ff6a0ec8 --- /dev/null +++ b/plugins/launcher-box/launcher-item.c @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "launcher-item.h" + +#include + +/** + * PhoshLauncherItem: + * + * A launcher item keeps the app-info and the launcher's indicator state together + */ + +enum { + PROP_0, + PROP_APP_INFO, + PROP_PROGRESS_VISIBLE, + PROP_PROGRESS, + PROP_COUNT_VISIBLE, + PROP_COUNT, + PROP_HAS_DATA, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshLauncherItem { + GObject parent; + + GDesktopAppInfo *app_info; + gboolean progress_visible; + double progress; + gboolean count_visible; + gint64 count; +}; +G_DEFINE_TYPE (PhoshLauncherItem, phosh_launcher_item, G_TYPE_OBJECT) + + +static void +phosh_launcher_item_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLauncherItem *self = PHOSH_LAUNCHER_ITEM (object); + + switch (property_id) { + case PROP_APP_INFO: + g_set_object (&self->app_info, g_value_get_object (value)); + break; + case PROP_PROGRESS_VISIBLE: + phosh_launcher_item_set_progress_visible (self, g_value_get_boolean (value)); + break; + case PROP_PROGRESS: + phosh_launcher_item_set_progress (self, g_value_get_double (value)); + break; + case PROP_COUNT_VISIBLE: + phosh_launcher_item_set_count_visible (self, g_value_get_boolean (value)); + break; + case PROP_COUNT: + phosh_launcher_item_set_count (self, g_value_get_int64 (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_launcher_item_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLauncherItem *self = PHOSH_LAUNCHER_ITEM (object); + + switch (property_id) { + case PROP_APP_INFO: + g_value_set_object (value, self->app_info); + break; + case PROP_PROGRESS_VISIBLE: + g_value_set_boolean (value, phosh_launcher_item_get_progress_visible (self)); + break; + case PROP_PROGRESS: + g_value_set_double (value, phosh_launcher_item_get_progress (self)); + break; + case PROP_COUNT_VISIBLE: + g_value_set_boolean (value, phosh_launcher_item_get_count_visible (self)); + break; + case PROP_COUNT: + g_value_set_int64 (value, phosh_launcher_item_get_count (self)); + break; + case PROP_HAS_DATA: + g_value_set_boolean (value, self->progress_visible || self->count_visible); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_launcher_item_dispose (GObject *object) +{ + PhoshLauncherItem *self = PHOSH_LAUNCHER_ITEM (object); + + g_clear_object (&self->app_info); + + G_OBJECT_CLASS (phosh_launcher_item_parent_class)->dispose (object); +} + + +static void +phosh_launcher_item_class_init (PhoshLauncherItemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_launcher_item_get_property; + object_class->set_property = phosh_launcher_item_set_property; + object_class->dispose = phosh_launcher_item_dispose; + + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", "", "", + G_TYPE_DESKTOP_APP_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_PROGRESS_VISIBLE] = + g_param_spec_boolean ("progress-visible", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_PROGRESS] = + g_param_spec_double ("progress", "", "", + 0.0, 1.0, 0.0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_COUNT_VISIBLE] = + g_param_spec_boolean ("count-visible", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_COUNT] = + g_param_spec_int64 ("count", "", "", + 0, G_MAXINT64, G_MAXINT64, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * PhoshLauncherItem:has-data: + * + * The launcher item wants to show data (either count, progress or both). + */ + props[PROP_HAS_DATA] = + g_param_spec_boolean ("has-data", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_launcher_item_init (PhoshLauncherItem *self) +{ +} + + +PhoshLauncherItem * +phosh_launcher_item_new (GDesktopAppInfo *app_info) +{ + return g_object_new (PHOSH_TYPE_LAUNCHER_ITEM, "app-info", app_info, NULL); +} + + +gboolean +phosh_launcher_item_get_progress_visible (PhoshLauncherItem *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ITEM (self), FALSE); + + return self->progress_visible; +} + + +void +phosh_launcher_item_set_progress_visible (PhoshLauncherItem *self, gboolean visible) +{ + g_return_if_fail (PHOSH_IS_LAUNCHER_ITEM (self)); + + if (self->progress_visible == visible) + return; + + self->progress_visible = visible; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PROGRESS_VISIBLE]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_DATA]); +} + + +double +phosh_launcher_item_get_progress (PhoshLauncherItem *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ITEM (self), 0.0); + + return self->progress; +} + + +void +phosh_launcher_item_set_progress (PhoshLauncherItem *self, double progress) +{ + g_return_if_fail (PHOSH_IS_LAUNCHER_ITEM (self)); + + if (G_APPROX_VALUE (self->progress, progress, FLT_EPSILON)) + return; + + self->progress = progress; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PROGRESS]); +} + + +gboolean +phosh_launcher_item_get_count_visible (PhoshLauncherItem *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ITEM (self), FALSE); + + return self->count_visible; +} + + +void +phosh_launcher_item_set_count_visible (PhoshLauncherItem *self, gboolean visible) +{ + g_return_if_fail (PHOSH_IS_LAUNCHER_ITEM (self)); + + if (self->count_visible == visible) + return; + + self->count_visible = visible; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COUNT_VISIBLE]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_DATA]); +} + + +void +phosh_launcher_item_set_count (PhoshLauncherItem *self, gint64 count) +{ + g_return_if_fail (PHOSH_IS_LAUNCHER_ITEM (self)); + + if (count == self->count) + return; + + self->count = count; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COUNT]); +} + + +gint64 +phosh_launcher_item_get_count (PhoshLauncherItem *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ITEM (self), 0); + + return self->count; +} + + +GDesktopAppInfo * +phosh_launcher_item_get_app_info (PhoshLauncherItem *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ITEM (self), NULL); + + return self->app_info; +} diff --git a/plugins/launcher-box/launcher-item.h b/plugins/launcher-box/launcher-item.h new file mode 100644 index 000000000..52a1ccbad --- /dev/null +++ b/plugins/launcher-box/launcher-item.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LAUNCHER_ITEM (phosh_launcher_item_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLauncherItem, phosh_launcher_item, PHOSH, LAUNCHER_ITEM, GObject) + +PhoshLauncherItem *phosh_launcher_item_new (GDesktopAppInfo *app_info); +gboolean phosh_launcher_item_get_progress_visible (PhoshLauncherItem *self); +void phosh_launcher_item_set_progress_visible (PhoshLauncherItem *self, gboolean visible); +double phosh_launcher_item_get_progress (PhoshLauncherItem *self); +void phosh_launcher_item_set_progress (PhoshLauncherItem *self, double progress); +gboolean phosh_launcher_item_get_count_visible (PhoshLauncherItem *self); +void phosh_launcher_item_set_count_visible (PhoshLauncherItem *self, gboolean visible); +gint64 phosh_launcher_item_get_count (PhoshLauncherItem *self); +void phosh_launcher_item_set_count (PhoshLauncherItem *self, gint64 count); +GDesktopAppInfo *phosh_launcher_item_get_app_info (PhoshLauncherItem *self); + +G_END_DECLS diff --git a/plugins/launcher-box/launcher-row.c b/plugins/launcher-box/launcher-row.c new file mode 100644 index 000000000..3022e0472 --- /dev/null +++ b/plugins/launcher-box/launcher-row.c @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "launcher-row.h" + +#include +#include + + +enum { + PROP_0, + PROP_LAUNCHER_ITEM, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshLauncherRow { + HdyActionRow parent; + + PhoshLauncherItem *item; + + GtkWidget *count_label; + GtkWidget *progress_bar; + GtkWidget *box_data; +}; +G_DEFINE_TYPE (PhoshLauncherRow, phosh_launcher_row, HDY_TYPE_ACTION_ROW) + + +static gboolean +transform_count_to_label (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gint64 count = g_value_get_int64 (from_value); + char *label; + + label = g_strdup_printf ("%" G_GUINT64_FORMAT, count); + g_value_take_string (to_value, label); + return TRUE; +} + + +static void +set_item (PhoshLauncherRow *self, PhoshLauncherItem *item) +{ + g_autofree char *icon_name = NULL; + const char *desc = NULL; + GDesktopAppInfo *info; + + g_assert (!self->item); + + self->item = g_object_ref (item); + info = phosh_launcher_item_get_app_info (item); + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), + g_app_info_get_display_name (G_APP_INFO (info))); + + icon_name = g_desktop_app_info_get_string (info, "Icon"); + if (icon_name) + hdy_action_row_set_icon_name (HDY_ACTION_ROW (self), icon_name); + + desc = g_app_info_get_description (G_APP_INFO (info)); + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), desc); + hdy_action_row_set_subtitle_lines (HDY_ACTION_ROW (self), 1); + + g_object_bind_property (self->item, "progress-visible", + self->progress_bar, "visible", + G_BINDING_SYNC_CREATE); + g_object_bind_property (self->item, "progress", + self->progress_bar, "value", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->item, "count-visible", + self->count_label, "visible", + G_BINDING_SYNC_CREATE); + g_object_bind_property_full (self->item, "count", + self->count_label, "label", + G_BINDING_SYNC_CREATE, + transform_count_to_label, + NULL, NULL, NULL); + + g_object_bind_property (self->item, "has-data", + self->box_data, "visible", + G_BINDING_SYNC_CREATE); +} + + +static void +phosh_launcher_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLauncherRow *self = PHOSH_LAUNCHER_ROW (object); + + switch (property_id) { + case PROP_LAUNCHER_ITEM: + set_item (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_launcher_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLauncherRow *self = PHOSH_LAUNCHER_ROW (object); + + switch (property_id) { + case PROP_LAUNCHER_ITEM: + g_value_set_object (value, self->item); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_launcher_row_finalize (GObject *object) +{ + PhoshLauncherRow *self = PHOSH_LAUNCHER_ROW (object); + + g_clear_object (&self->item); + + G_OBJECT_CLASS (phosh_launcher_row_parent_class)->finalize (object); +} + + +static void +phosh_launcher_row_class_init (PhoshLauncherRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_launcher_row_get_property; + object_class->set_property = phosh_launcher_row_set_property; + object_class->finalize = phosh_launcher_row_finalize; + + props[PROP_LAUNCHER_ITEM] = + g_param_spec_object ("launcher-item", "", "", + PHOSH_TYPE_LAUNCHER_ITEM, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/launcher-box/launcher-row.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshLauncherRow, box_data); + gtk_widget_class_bind_template_child (widget_class, PhoshLauncherRow, count_label); + gtk_widget_class_bind_template_child (widget_class, PhoshLauncherRow, progress_bar); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_launcher_row_init (PhoshLauncherRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_launcher_row_new (PhoshLauncherItem *app_info) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_LAUNCHER_ROW, "launcher-item", app_info, NULL)); +} + + +PhoshLauncherItem * +phosh_launcher_row_get_item (PhoshLauncherRow *self) +{ + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ROW (self), NULL); + + return self->item; +} diff --git a/plugins/launcher-box/launcher-row.h b/plugins/launcher-box/launcher-row.h new file mode 100644 index 000000000..f57521156 --- /dev/null +++ b/plugins/launcher-box/launcher-row.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "launcher-item.h" + +#include +#include + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LAUNCHER_ROW (phosh_launcher_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLauncherRow, phosh_launcher_row, PHOSH, LAUNCHER_ROW, HdyActionRow) + +GtkWidget *phosh_launcher_row_new (PhoshLauncherItem *item); +PhoshLauncherItem *phosh_launcher_row_get_item (PhoshLauncherRow *self); + +G_END_DECLS diff --git a/plugins/launcher-box/launcher-row.ui b/plugins/launcher-box/launcher-row.ui new file mode 100644 index 000000000..5d9ccd593 --- /dev/null +++ b/plugins/launcher-box/launcher-row.ui @@ -0,0 +1,33 @@ + + + + + diff --git a/plugins/launcher-box/meson.build b/plugins/launcher-box/meson.build new file mode 100644 index 000000000..e96197a43 --- /dev/null +++ b/plugins/launcher-box/meson.build @@ -0,0 +1,59 @@ +name = 'launcher-box' + +launcher_box_plugin_deps = [plugin_dep, gio_unix_dep] + +launcher_box_resources = gnome.compile_resources( + 'phosh-plugin-launcher-box-resources', + 'phosh-plugin-launcher-box.gresources.xml', + c_name: 'phosh_plugin_launcher_box', +) + +launcher_box_plugin_sources = files( + 'launcher-box.c', + 'launcher-box.h', + 'launcher-item.c', + 'launcher-item.h', + 'launcher-row.c', + 'launcher-row.h', + 'phosh-plugin-launcher-box.c', +) + +phosh_launcher_box_plugin = shared_module( + 'phosh-plugin-launcher-box', + launcher_box_plugin_sources, + launcher_box_resources, + c_args: ['-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), '-DPLUGIN_NAME="@0@"'.format(name)], + dependencies: launcher_box_plugin_deps, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +launcher_box_schema = 'sm.puri.phosh.plugins.launcher-box.gschema.xml' +compiled = gnome.compile_schemas(depend_files: launcher_box_schema) +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test( + 'Validate @0@ schema file'.format(launcher_box_schema), + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()], + ) +endif +install_data(launcher_box_schema, install_dir: 'share/glib-2.0/schemas') diff --git a/plugins/launcher-box/phosh-plugin-launcher-box.c b/plugins/launcher-box/phosh-plugin-launcher-box.c new file mode 100644 index 000000000..f205f5ec8 --- /dev/null +++ b/plugins/launcher-box/phosh-plugin-launcher-box.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "launcher-box.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_launcher_box_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_LAUNCHER_BOX, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_launcher_box_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/launcher-box/phosh-plugin-launcher-box.gresources.xml b/plugins/launcher-box/phosh-plugin-launcher-box.gresources.xml new file mode 100644 index 000000000..8bb1d365f --- /dev/null +++ b/plugins/launcher-box/phosh-plugin-launcher-box.gresources.xml @@ -0,0 +1,8 @@ + + + + launcher-box.ui + launcher-row.ui + stylesheet/common.css + + diff --git a/plugins/launcher-box/sm.puri.phosh.plugins.launcher-box.gschema.xml b/plugins/launcher-box/sm.puri.phosh.plugins.launcher-box.gschema.xml new file mode 100644 index 000000000..602309e2f --- /dev/null +++ b/plugins/launcher-box/sm.puri.phosh.plugins.launcher-box.gschema.xml @@ -0,0 +1,13 @@ + + + + '' + Folder containing the launchers + + The folder that contains the launchers that should be available + on the lock screen. If unset defaults to $XDG_CONFIG_HOME/phosh/plugins/launcher-box/ + + + + diff --git a/plugins/launcher-box/stylesheet/common.css b/plugins/launcher-box/stylesheet/common.css new file mode 100644 index 000000000..e57acdfee --- /dev/null +++ b/plugins/launcher-box/stylesheet/common.css @@ -0,0 +1,12 @@ +phosh-launcher-box { + background-color: @phosh_notification_bg_color; + border-radius: 12px; + padding: 8px; +} + +phosh-launcher-box row label.p-launcher-row-count { + border-radius: 12px; + padding: 2px 6px 2px 6px; + background: shade(@theme_bg_color, 1.2); + color: shade(@theme_fg_color, 0.9); +} diff --git a/plugins/location-quick-setting/icons/location-quick-setting-symbolic.svg b/plugins/location-quick-setting/icons/location-quick-setting-symbolic.svg new file mode 100644 index 000000000..ac3bed276 --- /dev/null +++ b/plugins/location-quick-setting/icons/location-quick-setting-symbolic.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/location-quick-setting/location-quick-setting.c b/plugins/location-quick-setting/location-quick-setting.c new file mode 100644 index 000000000..9c05a5d11 --- /dev/null +++ b/plugins/location-quick-setting/location-quick-setting.c @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Gotam Gorabh + */ + +#include "location-quick-setting.h" +#include "quick-setting.h" +#include "status-icon.h" + +#include + +#define LOCATION_SETTINGS "org.gnome.system.location" +#define ENABLED_KEY "enabled" + +/** + * PhoshLocationQuickSetting: + * + * A quick setting to toggle location services on/off + */ +struct _PhoshLocationQuickSetting { + PhoshQuickSetting parent; + + GSettings *settings; + PhoshStatusIcon *info; +}; + +G_DEFINE_TYPE (PhoshLocationQuickSetting, phosh_location_quick_setting, PHOSH_TYPE_QUICK_SETTING); + +static void +on_clicked (PhoshLocationQuickSetting *self) +{ + gboolean enabled = phosh_quick_setting_get_active (PHOSH_QUICK_SETTING (self)); + + phosh_quick_setting_set_active (PHOSH_QUICK_SETTING (self), !enabled); +} + + +static gboolean +transform_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *icon_name; + + icon_name = enabled ? "location-services-active-symbolic" : "location-services-disabled-symbolic"; + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static gboolean +transform_to_label (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *label; + + label = enabled ? _("Location On") : _("Location Off"); + g_value_set_string (to_value, label); + return TRUE; +} + +static void +phosh_location_quick_setting_finalize (GObject *object) +{ + PhoshLocationQuickSetting *self = PHOSH_LOCATION_QUICK_SETTING (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_location_quick_setting_parent_class)->finalize (object); +} + + +static void +phosh_location_quick_setting_class_init (PhoshLocationQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_location_quick_setting_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/location-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshLocationQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_location_quick_setting_init (PhoshLocationQuickSetting *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (LOCATION_SETTINGS); + + g_settings_bind (self->settings, "enabled", + self, "active", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self, "active", + self->info, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_icon_name, + NULL, NULL, NULL); + + g_object_bind_property_full (self, "active", + self->info, "info", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_label, + NULL, NULL, NULL); +} diff --git a/plugins/location-quick-setting/location-quick-setting.desktop.in.in b/plugins/location-quick-setting/location-quick-setting.desktop.in.in new file mode 100644 index 000000000..5a0e762e2 --- /dev/null +++ b/plugins/location-quick-setting/location-quick-setting.desktop.in.in @@ -0,0 +1,9 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Location Quick Setting +Types=quick-setting; +Comment=Toggle location services on/off +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=location-quick-setting + diff --git a/plugins/location-quick-setting/location-quick-setting.h b/plugins/location-quick-setting/location-quick-setting.h new file mode 100644 index 000000000..712080fd4 --- /dev/null +++ b/plugins/location-quick-setting/location-quick-setting.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Gotam Gorabh + */ + + #pragma once + + #include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LOCATION_QUICK_SETTING phosh_location_quick_setting_get_type () +G_DECLARE_FINAL_TYPE (PhoshLocationQuickSetting, + phosh_location_quick_setting, + PHOSH, LOCATION_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/location-quick-setting/meson.build b/plugins/location-quick-setting/meson.build new file mode 100644 index 000000000..e04acc8ce --- /dev/null +++ b/plugins/location-quick-setting/meson.build @@ -0,0 +1,47 @@ +name = 'location-quick-setting' + +location_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-location-quick-setting-resources', + 'phosh-plugin-location-quick-setting.gresources.xml', + c_name: 'phosh_plugin_location_quick_setting', +) + +location_quick_setting_plugin_sources = files( + 'location-quick-setting.c', + 'phosh-plugin-location-quick-setting.c', +) + +phosh_location_quick_setting_plugin = shared_module( + 'phosh-plugin-location-quick-setting', + location_quick_setting_plugin_sources, + location_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/location-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, +) diff --git a/plugins/location-quick-setting/phosh-plugin-location-quick-setting.c b/plugins/location-quick-setting/phosh-plugin-location-quick-setting.c new file mode 100644 index 000000000..f82ad6015 --- /dev/null +++ b/plugins/location-quick-setting/phosh-plugin-location-quick-setting.c @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Gotam Gorabh + */ + +#include "location-quick-setting.h" +#include "phosh-plugin.h" + +#include +#include + +char **g_io_phosh_plugin_location_quick_setting_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_LOCATION_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + +char ** +g_io_phosh_plugin_location_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/location-quick-setting/phosh-plugin-location-quick-setting.gresources.xml b/plugins/location-quick-setting/phosh-plugin-location-quick-setting.gresources.xml new file mode 100644 index 000000000..9ff154c1c --- /dev/null +++ b/plugins/location-quick-setting/phosh-plugin-location-quick-setting.gresources.xml @@ -0,0 +1,6 @@ + + + + qs.ui + + diff --git a/plugins/location-quick-setting/qs.ui b/plugins/location-quick-setting/qs.ui new file mode 100644 index 000000000..92a11b056 --- /dev/null +++ b/plugins/location-quick-setting/qs.ui @@ -0,0 +1,12 @@ + + + + + + 1 + 16 + + diff --git a/plugins/media-players/media-players.c b/plugins/media-players/media-players.c new file mode 100644 index 000000000..68412526f --- /dev/null +++ b/plugins/media-players/media-players.c @@ -0,0 +1,118 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "media-player.h" +#include "media-players.h" +#include "mpris-manager.h" +#include "plugin-shell.h" + +/** + * PhoshMediaPlayers + * + * Show all currently running media players + */ +struct _PhoshMediaPlayers { + GtkBox parent; + + GListModel *known_players; + + GtkListBox *media_players_list_box; + GtkStack *media_players_stack; +}; + +G_DEFINE_TYPE (PhoshMediaPlayers, phosh_media_players, GTK_TYPE_BOX); + + +static void +on_n_items_changed (PhoshMediaPlayers *self) +{ + const char *stack_page = "no-media-players"; + guint n_players; + + n_players = g_list_model_get_n_items (self->known_players); + g_debug ("Tracking %u players", n_players); + + if (n_players) + stack_page = "media-players"; + + gtk_stack_set_visible_child_name (self->media_players_stack, stack_page); +} + + +static void +phosh_media_players_finalize (GObject *object) +{ + PhoshMediaPlayers *self = PHOSH_MEDIA_PLAYERS (object); + + g_clear_object (&self->known_players); + + G_OBJECT_CLASS (phosh_media_players_parent_class)->finalize (object); +} + + +static void +phosh_media_players_class_init (PhoshMediaPlayersClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_media_players_finalize; + + g_type_ensure (PHOSH_TYPE_MEDIA_PLAYER); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/media-players/media-players.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshMediaPlayers, media_players_list_box); + gtk_widget_class_bind_template_child (widget_class, PhoshMediaPlayers, media_players_stack); + + gtk_widget_class_set_css_name (widget_class, "phosh-media-players"); +} + + +static GtkWidget * +create_media_player_row (gpointer item, gpointer user_data) +{ + PhoshDBusMediaPlayer2Player *mpris_player = PHOSH_DBUS_MEDIA_PLAYER2_PLAYER (item); + GtkWidget *media_player = phosh_media_player_new (); + + g_debug ("Adding new player %s", g_dbus_proxy_get_name (G_DBUS_PROXY (mpris_player))); + + phosh_media_player_set_player (PHOSH_MEDIA_PLAYER (media_player), mpris_player); + + return media_player; +} + + +static void +phosh_media_players_init (PhoshMediaPlayers *self) +{ + PhoshMprisManager *manager = phosh_shell_get_mpris_manager (phosh_shell_get_default ()); + g_autoptr (GtkCssProvider) css_provider = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->known_players = g_object_ref (phosh_mpris_manager_get_known_players (manager)); + gtk_list_box_bind_model (self->media_players_list_box, + G_LIST_MODEL (self->known_players), + create_media_player_row, + NULL, + NULL); + g_signal_connect_object (self->known_players, + "items-changed", + G_CALLBACK (on_n_items_changed), + self, + G_CONNECT_SWAPPED); + on_n_items_changed (self); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/media-players/stylesheet/common.css"); + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self)), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); +} diff --git a/plugins/media-players/media-players.desktop.in.in b/plugins/media-players/media-players.desktop.in.in new file mode 100644 index 000000000..d5e6c39a8 --- /dev/null +++ b/plugins/media-players/media-players.desktop.in.in @@ -0,0 +1,6 @@ +[Plugin] +Id=@name@ +Name=Media Players +Types=lockscreen; +Comment=Track currently running media players +Plugin=@plugins_dir@/libphosh-plugin-@name@.so diff --git a/plugins/media-players/media-players.h b/plugins/media-players/media-players.h new file mode 100644 index 000000000..220c72866 --- /dev/null +++ b/plugins/media-players/media-players.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MEDIA_PLAYERS (phosh_media_players_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshMediaPlayers, phosh_media_players, PHOSH, MEDIA_PLAYERS, GtkBox) + +G_END_DECLS diff --git a/plugins/media-players/media-players.ui b/plugins/media-players/media-players.ui new file mode 100644 index 000000000..b6f67d70b --- /dev/null +++ b/plugins/media-players/media-players.ui @@ -0,0 +1,55 @@ + + + + + diff --git a/plugins/media-players/meson.build b/plugins/media-players/meson.build new file mode 100644 index 000000000..fff33a00a --- /dev/null +++ b/plugins/media-players/meson.build @@ -0,0 +1,46 @@ +name = 'media-players' + +media_players_plugin_deps = [plugin_dep, gio_unix_dep] + +media_players_resources = gnome.compile_resources( + 'phosh-plugin-media-players-resources', + 'phosh-plugin-media-players.gresources.xml', + c_name: 'phosh_plugin_media_players', +) + +media_players_plugin_sources = files( + 'media-players.c', + 'media-players.h', + 'phosh-plugin-media-players.c', +) + +phosh_media_players_plugin = shared_module( + 'phosh-plugin-media-players', + media_players_plugin_sources, + media_players_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: media_players_plugin_deps, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) diff --git a/plugins/media-players/phosh-plugin-media-players.c b/plugins/media-players/phosh-plugin-media-players.c new file mode 100644 index 000000000..66aa99af7 --- /dev/null +++ b/plugins/media-players/phosh-plugin-media-players.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "media-players.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_media_players_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_MEDIA_PLAYERS, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_media_players_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/media-players/phosh-plugin-media-players.gresources.xml b/plugins/media-players/phosh-plugin-media-players.gresources.xml new file mode 100644 index 000000000..911e8cf2b --- /dev/null +++ b/plugins/media-players/phosh-plugin-media-players.gresources.xml @@ -0,0 +1,7 @@ + + + + media-players.ui + stylesheet/common.css + + diff --git a/plugins/media-players/stylesheet/common.css b/plugins/media-players/stylesheet/common.css new file mode 100644 index 000000000..e69de29bb diff --git a/plugins/meson.build b/plugins/meson.build new file mode 100644 index 000000000..d09b52e0b --- /dev/null +++ b/plugins/meson.build @@ -0,0 +1,126 @@ +phosh_plugin_header_subdir = 'phosh' + +adw_ver = '1.5' +gtk4_ver = '4.12' + +adw_ver_str = 'ADW_VERSION_@0@'.format(adw_ver.replace('.', '_')) +gtk4_ver_str = 'ADW_VERSION_@0@'.format(gtk4_ver.replace('.', '_')) +adw_ver_cmp = '>=@0@'.format(adw_ver) +gtk4_ver_cmp = '>=@0@'.format(gtk4_ver) + +# Public header that can also be used by out of tree plugins +phosh_plugin_config = configuration_data() +phosh_plugin_config.set_quoted( + 'PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET', + 'phosh-lockscreen-widget', +) +phosh_plugin_config.set_quoted( + 'PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS', + 'phosh-lockscreen-widget-prefs', +) +phosh_plugin_config.set_quoted( + 'PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET', + 'phosh-quick-setting-widget', +) +phosh_plugin_config.set_quoted( + 'PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS', + 'phosh-quick-setting-widget-prefs', +) + +phosh_plugin_h = configure_file( + input: 'phosh-plugin.h.in', + output: 'phosh-plugin.h', + configuration: phosh_plugin_config, +) +install_headers([phosh_plugin_h], subdir: phosh_plugin_header_subdir) + +pkgconfig.generate( + variables: [ + 'lockscreen_plugins_dir=@0@'.format(plugins_dir), + 'lockscreen_prefs_dir=@0@'.format(plugin_prefs_dir), + 'quick_setting_plugins_dir=@0@'.format(plugins_dir), + 'quick_setting_prefs_dir=@0@'.format(plugin_prefs_dir), + ], + subdirs: phosh_plugin_header_subdir, + version: meson.project_version(), + install_dir: libdir / 'pkgconfig', + filebase: 'phosh-plugins', + name: 'phosh-plugins', + description: 'Phosh plugin configuration data', +) + +lockscreen_plugins = [ + 'calendar', + 'emergency-info', + 'launcher-box', + 'media-players', + 'ticket-box', + 'upcoming-events', +] + +quick_setting_plugins = [ + 'caffeine-quick-setting', + 'dark-mode-quick-setting', + 'location-quick-setting', + 'mobile-data-quick-setting', + 'night-light-quick-setting', + 'pomodoro-quick-setting', + 'scaling-quick-setting', + 'simple-custom-quick-setting', + 'wifi-hotspot-quick-setting', +] + +plugin_schema_dirs = [] +foreach plugin : lockscreen_plugins + quick_setting_plugins + plugin_schema_dirs += meson.project_build_root() / 'plugins' / plugin +endforeach +plugin_schema_path = ':'.join(plugin_schema_dirs) + +if get_option('lockscreen-plugins') or get_option('quick-setting-plugins') + plugin_inc = include_directories('.') + plugin_prefs_inc = include_directories('.') + + # Like phosh-config.h but for preference plugins + plugin_prefs_config = configuration_data() + plugin_prefs_config.set_quoted('GETTEXT_PACKAGE', 'phosh') + plugin_prefs_config.set_quoted('LOCALEDIR', localedir) + plugin_prefs_config_h = configure_file( + output: 'phosh-plugin-prefs-config.h', + configuration: plugin_prefs_config, + ) + + plugin_dep = declare_dependency( + sources: phosh_plugin_h, + include_directories: plugin_inc, + dependencies: [ + gio_unix_dep, + gobject_dep, + gtk_dep, + libhandy_dep, + libnm_dep, + phosh_plugins_dep, + ], + ) + + plugin_prefs_dep = declare_dependency( + sources: [phosh_plugin_h, plugin_prefs_config_h], + include_directories: plugin_prefs_inc, + dependencies: [ + dependency('libadwaita-1', version: adw_ver_cmp), + dependency('gtk4', version: gtk4_ver_cmp), + ], + ) + + if get_option('lockscreen-plugins') + foreach plugin : lockscreen_plugins + subdir(plugin) + endforeach + endif + + if get_option('quick-setting-plugins') + foreach plugin : quick_setting_plugins + subdir(plugin) + endforeach + endif + +endif diff --git a/plugins/mobile-data-quick-setting/icons/mobile-data-quick-setting-symbolic.svg b/plugins/mobile-data-quick-setting/icons/mobile-data-quick-setting-symbolic.svg new file mode 120000 index 000000000..70af94288 --- /dev/null +++ b/plugins/mobile-data-quick-setting/icons/mobile-data-quick-setting-symbolic.svg @@ -0,0 +1 @@ +../../../data/icons/mobile-data-symbolic.svg \ No newline at end of file diff --git a/plugins/mobile-data-quick-setting/meson.build b/plugins/mobile-data-quick-setting/meson.build new file mode 100644 index 000000000..cfc519166 --- /dev/null +++ b/plugins/mobile-data-quick-setting/meson.build @@ -0,0 +1,48 @@ +name = 'mobile-data-quick-setting' + +mobile_data_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-mobile-data-quick-setting-resources', + 'phosh-plugin-mobile-data-quick-setting.gresources.xml', + c_name: 'phosh_plugin_mobile_data_quick_setting', +) + +mobile_data_quick_setting_plugin_sources = files( + 'mobile-data-quick-setting.c', + 'phosh-plugin-mobile-data-quick-setting.c', +) + +phosh_mobile_data_quick_setting_plugin = shared_module( + 'phosh-plugin-mobile-data-quick-setting', + mobile_data_quick_setting_plugin_sources, + mobile_data_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/mobile-data-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, + follow_symlinks: true, +) diff --git a/plugins/mobile-data-quick-setting/mobile-data-quick-setting.c b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.c new file mode 100644 index 000000000..437a20bac --- /dev/null +++ b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.c @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "mobile-data-quick-setting.h" +#include "plugin-shell.h" + +#include + +/** + * PhoshMobileDataQuickSetting: + * + * Toggle mobile data + */ + +struct _PhoshMobileDataQuickSetting { + PhoshQuickSetting parent; + + PhoshStatusIcon *info; +}; + +G_DEFINE_TYPE (PhoshMobileDataQuickSetting, phosh_mobile_data_quick_setting, PHOSH_TYPE_QUICK_SETTING); + + +static void +toggle_sensitive_cb (PhoshMobileDataQuickSetting *self) +{ + PhoshWWan *wwan = phosh_shell_get_wwan (phosh_shell_get_default ()); + gboolean has_data, unlocked, enabled, sensitive; + + has_data = phosh_wwan_has_data (wwan); + unlocked = phosh_wwan_is_unlocked (wwan); + enabled = phosh_wwan_is_enabled (wwan); + + sensitive = has_data && unlocked && enabled; + gtk_widget_set_sensitive (GTK_WIDGET (self), sensitive); +} + + +static void +on_clicked (PhoshMobileDataQuickSetting *self) +{ + PhoshWWan *wwan = phosh_shell_get_wwan (phosh_shell_get_default ()); + gboolean enabled; + + enabled = phosh_quick_setting_get_active (PHOSH_QUICK_SETTING (self)); + phosh_wwan_set_data_enabled (wwan, !enabled); +} + + +static gboolean +transform_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *icon_name; + + icon_name = enabled ? "mobile-data-symbolic" : "mobile-data-disabled-symbolic"; + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static gboolean +transform_to_label (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *label; + + label = enabled ? _("Mobile Data On") : _("Mobile Data Off"); + g_value_set_string (to_value, label); + return TRUE; +} + + +static void +phosh_mobile_data_quick_setting_class_init (PhoshMobileDataQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/mobile-data-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshMobileDataQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_mobile_data_quick_setting_init (PhoshMobileDataQuickSetting *self) +{ + PhoshWWan *wwan; + + gtk_widget_init_template (GTK_WIDGET (self)); + + wwan = phosh_shell_get_wwan (phosh_shell_get_default ()); + + g_object_bind_property (wwan, "data-enabled", + self, "active", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self, "active", + self->info, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_icon_name, + NULL, NULL, NULL); + + g_object_bind_property_full (self, "active", + self->info, "info", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_label, + NULL, NULL, NULL); + + g_object_connect (wwan, + "swapped-object-signal::notify::enabled", + toggle_sensitive_cb, + self, + "swapped-object-signal::notify::has-data", + toggle_sensitive_cb, + self, + "swapped-object-signal::notify::unlocked", + toggle_sensitive_cb, + self, + NULL); +} diff --git a/plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in new file mode 100644 index 000000000..610b913ec --- /dev/null +++ b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Mobile Data Quick Setting +Types=quick-setting; +Comment=Toggle mobile data on/off +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=mobile-data-quick-setting diff --git a/plugins/mobile-data-quick-setting/mobile-data-quick-setting.h b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.h new file mode 100644 index 000000000..ea91fd4d8 --- /dev/null +++ b/plugins/mobile-data-quick-setting/mobile-data-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MOBILE_DATA_QUICK_SETTING phosh_mobile_data_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshMobileDataQuickSetting, + phosh_mobile_data_quick_setting, + PHOSH, MOBILE_DATA_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.c b/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.c new file mode 100644 index 000000000..ec8e9eb26 --- /dev/null +++ b/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.c @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "mobile-data-quick-setting.h" +#include "phosh-plugin.h" + + +char **g_io_phosh_plugin_mobile_data_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_MOBILE_DATA_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_mobile_data_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.gresources.xml b/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.gresources.xml new file mode 100644 index 000000000..1beabfb79 --- /dev/null +++ b/plugins/mobile-data-quick-setting/phosh-plugin-mobile-data-quick-setting.gresources.xml @@ -0,0 +1,6 @@ + + + + qs.ui + + diff --git a/plugins/mobile-data-quick-setting/qs.ui b/plugins/mobile-data-quick-setting/qs.ui new file mode 100644 index 000000000..41c1f7e88 --- /dev/null +++ b/plugins/mobile-data-quick-setting/qs.ui @@ -0,0 +1,12 @@ + + + + + + 1 + 16 + + diff --git a/plugins/night-light-quick-setting/icons/night-light-quick-setting-symbolic.svg b/plugins/night-light-quick-setting/icons/night-light-quick-setting-symbolic.svg new file mode 100644 index 000000000..be4c97f46 --- /dev/null +++ b/plugins/night-light-quick-setting/icons/night-light-quick-setting-symbolic.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/plugins/night-light-quick-setting/meson.build b/plugins/night-light-quick-setting/meson.build new file mode 100644 index 000000000..877ef0f7c --- /dev/null +++ b/plugins/night-light-quick-setting/meson.build @@ -0,0 +1,41 @@ +name = 'night-light-quick-setting' + +night_light_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-night-light-quick-setting-resources', + 'phosh-plugin-night-light-quick-setting.gresources.xml', + c_name: 'phosh_plugin_night_light_quick_setting', +) + +night_light_quick_setting_plugin_sources = files( + 'night-light-quick-setting.c', + 'phosh-plugin-night-light-quick-setting.c', +) + +phosh_night_light_quick_setting_plugin = shared_module( + 'phosh-plugin-night-light-quick-setting', + night_light_quick_setting_plugin_sources, + night_light_quick_setting_resources, + c_args: ['-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), '-DPLUGIN_NAME="@0@"'.format(name)], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data('icons/night-light-quick-setting-symbolic.svg', install_dir: phosh_plugin_icon_dir) diff --git a/plugins/night-light-quick-setting/night-light-quick-setting.c b/plugins/night-light-quick-setting/night-light-quick-setting.c new file mode 100644 index 000000000..8b8135383 --- /dev/null +++ b/plugins/night-light-quick-setting/night-light-quick-setting.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "night-light-quick-setting.h" +#include "plugin-shell.h" + +#include + +/** + * PhoshNightLightQuickSetting: + * + * Enable and disable nightlight. + */ + +struct _PhoshNightLightQuickSetting { + PhoshQuickSetting parent; + + GSettings *settings; + PhoshStatusIcon *info; +}; + +G_DEFINE_TYPE (PhoshNightLightQuickSetting, phosh_night_light_quick_setting, PHOSH_TYPE_QUICK_SETTING); + + +static void +on_clicked (PhoshNightLightQuickSetting *self) +{ + gboolean enabled = phosh_quick_setting_get_active (PHOSH_QUICK_SETTING (self)); + + phosh_quick_setting_set_active (PHOSH_QUICK_SETTING (self), !enabled); +} + + +static gboolean +transform_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *icon_name; + + icon_name = enabled ? "night-light-symbolic" : "night-light-disabled-symbolic"; + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static gboolean +transform_to_label (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean enabled = g_value_get_boolean (from_value); + const char *label; + + label = enabled ? _("Night Light On") : _("Night Light Off"); + g_value_set_string (to_value, label); + return TRUE; +} + + +static void +phosh_lockscreen_finalize (GObject *object) +{ + PhoshNightLightQuickSetting *self = PHOSH_NIGHT_LIGHT_QUICK_SETTING (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_night_light_quick_setting_parent_class)->finalize (object); +} + + +static void +phosh_night_light_quick_setting_class_init (PhoshNightLightQuickSettingClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_lockscreen_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/night-light-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshNightLightQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_night_light_quick_setting_init (PhoshNightLightQuickSetting *self) +{ + PhoshMonitorManager *monitor_manager; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new ("org.gnome.settings-daemon.plugins.color"); + g_settings_bind (self->settings, "night-light-enabled", + self, "active", + G_SETTINGS_BIND_DEFAULT); + + g_object_bind_property_full (self, "active", + self->info, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_icon_name, + NULL, NULL, NULL); + + g_object_bind_property_full (self, "active", + self->info, "info", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_label, + NULL, NULL, NULL); + + monitor_manager = phosh_shell_get_monitor_manager (phosh_shell_get_default ()); + g_object_bind_property (monitor_manager, "night-light-supported", + self, "sensitive", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +} diff --git a/plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in b/plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in new file mode 100644 index 000000000..ae969cbb8 --- /dev/null +++ b/plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Night Light Quick Setting +Types=quick-setting; +Comment=Toggle night light on/off +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=night-light-quick-setting diff --git a/plugins/night-light-quick-setting/night-light-quick-setting.h b/plugins/night-light-quick-setting/night-light-quick-setting.h new file mode 100644 index 000000000..00a34fca7 --- /dev/null +++ b/plugins/night-light-quick-setting/night-light-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_NIGHT_LIGHT_QUICK_SETTING phosh_night_light_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshNightLightQuickSetting, + phosh_night_light_quick_setting, + PHOSH, NIGHT_LIGHT_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.c b/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.c new file mode 100644 index 000000000..c94d33632 --- /dev/null +++ b/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "night-light-quick-setting.h" +#include "phosh-plugin.h" + + +char **g_io_phosh_plugin_night_light_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_NIGHT_LIGHT_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_night_light_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.gresources.xml b/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.gresources.xml new file mode 100644 index 000000000..bb4fc4141 --- /dev/null +++ b/plugins/night-light-quick-setting/phosh-plugin-night-light-quick-setting.gresources.xml @@ -0,0 +1,6 @@ + + + + qs.ui + + diff --git a/plugins/night-light-quick-setting/qs.ui b/plugins/night-light-quick-setting/qs.ui new file mode 100644 index 000000000..5117eca22 --- /dev/null +++ b/plugins/night-light-quick-setting/qs.ui @@ -0,0 +1,12 @@ + + + + + + 1 + 16 + + diff --git a/plugins/phosh-plugin.h.in b/plugins/phosh-plugin.h.in new file mode 100644 index 000000000..696c8dc8b --- /dev/null +++ b/plugins/phosh-plugin.h.in @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2022 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#mesondefine PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET +#mesondefine PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS +#mesondefine PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET +#mesondefine PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS diff --git a/plugins/pomodoro-quick-setting/icons/pomodoro-active-symbolic.svg b/plugins/pomodoro-quick-setting/icons/pomodoro-active-symbolic.svg new file mode 100644 index 000000000..66cc41b3c --- /dev/null +++ b/plugins/pomodoro-quick-setting/icons/pomodoro-active-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/plugins/pomodoro-quick-setting/icons/pomodoro-break-symbolic.svg b/plugins/pomodoro-quick-setting/icons/pomodoro-break-symbolic.svg new file mode 100644 index 000000000..51fcd9e2f --- /dev/null +++ b/plugins/pomodoro-quick-setting/icons/pomodoro-break-symbolic.svg @@ -0,0 +1,2 @@ + + diff --git a/plugins/pomodoro-quick-setting/icons/pomodoro-off-symbolic.svg b/plugins/pomodoro-quick-setting/icons/pomodoro-off-symbolic.svg new file mode 100644 index 000000000..715793e6d --- /dev/null +++ b/plugins/pomodoro-quick-setting/icons/pomodoro-off-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/pomodoro-quick-setting/meson.build b/plugins/pomodoro-quick-setting/meson.build new file mode 100644 index 000000000..8fd55dc3a --- /dev/null +++ b/plugins/pomodoro-quick-setting/meson.build @@ -0,0 +1,74 @@ +name = 'pomodoro-quick-setting' + +pomodoro_enum_sources = gnome.mkenums_simple( + 'pomodoro-enums', + sources: 'pomodoro-quick-setting.h', +) + +pomodoro_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-pomodoro-quick-setting-resources', + 'phosh-plugin-pomodoro-quick-setting.gresources.xml', + c_name: 'phosh_plugin_pomodoro_quick_setting', +) + +pomodoro_quick_setting_plugin_sources = files( + 'phosh-plugin-pomodoro-quick-setting.c', + 'pomodoro-quick-setting.c', +) + +phosh_pomodoro_quick_setting_plugin = shared_module( + 'phosh-plugin-pomodoro-quick-setting', + pomodoro_quick_setting_plugin_sources, + pomodoro_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + sources: pomodoro_enum_sources, + dependencies: [plugin_dep, libcall_ui_dep], + include_directories: phosh_inc, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/pomodoro-active-symbolic.svg', + rename: 'pomodoro-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, +) + + +pomodoro_quick_setting_schema = 'mobi.phosh.plugins.pomodoro.gschema.xml' +compiled = gnome.compile_schemas(depend_files: pomodoro_quick_setting_schema) +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test( + 'Validate @0@ schema file'.format(pomodoro_quick_setting_schema), + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()], + ) +endif +install_data( + pomodoro_quick_setting_schema, + install_dir: 'share/glib-2.0/schemas', +) + +subdir('prefs') diff --git a/plugins/pomodoro-quick-setting/mobi.phosh.plugins.pomodoro.gschema.xml b/plugins/pomodoro-quick-setting/mobi.phosh.plugins.pomodoro.gschema.xml new file mode 100644 index 000000000..e1ec9de70 --- /dev/null +++ b/plugins/pomodoro-quick-setting/mobi.phosh.plugins.pomodoro.gschema.xml @@ -0,0 +1,26 @@ + + + + 1500 + Active interval duration + + The duration of the active interval in seconds. + + + + 300 + Break duration + + The duration of the break in seconds. + + + + false + Start on unlock + + Whether to start the timer on screen unlock + + + + diff --git a/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.c b/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.c new file mode 100644 index 000000000..6ed66abfb --- /dev/null +++ b/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-plugin.h" +#include "pomodoro-quick-setting.h" + + +char **g_io_phosh_plugin_pomodoro_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_POMODORO_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_pomodoro_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.gresources.xml b/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.gresources.xml new file mode 100644 index 000000000..ce8b687e5 --- /dev/null +++ b/plugins/pomodoro-quick-setting/phosh-plugin-pomodoro-quick-setting.gresources.xml @@ -0,0 +1,9 @@ + + + + qs.ui + icons/pomodoro-active-symbolic.svg + icons/pomodoro-break-symbolic.svg + icons/pomodoro-off-symbolic.svg + + diff --git a/plugins/pomodoro-quick-setting/pomodoro-quick-setting.c b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.c new file mode 100644 index 000000000..dfc1fcd52 --- /dev/null +++ b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.c @@ -0,0 +1,390 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "pomodoro-quick-setting.h" +#include "pomodoro-enums.h" +#include "plugin-shell.h" +#include "notify-manager.h" + +#include + +#include + +#define UPDATE_INTERVAL 1 /* seconds */ +#define ACTIVE_ICON "pomodoro-active-symbolic" +#define BREAK_ICON "pomodoro-break-symbolic" + +#define START_ON_UNLOCK_KEY "start-on-unlock" + +/** + * PhoshPomodoroQuickSetting: + * + * A quick setting for the Pomodoro technique + */ + +enum { + PROP_0, + PROP_STATE, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +struct _PhoshPomodoroQuickSetting { + PhoshQuickSetting parent; + + PhoshStatusIcon *info; + PhoshPomodoroState state; + int remaining; + guint update_id; + GSettings *settings; +}; + +G_DEFINE_TYPE (PhoshPomodoroQuickSetting, phosh_pomodoro_quick_setting, PHOSH_TYPE_QUICK_SETTING); + +static gboolean _started_on_login; + +static void +phosh_pomodoro_quick_setting_clear_timers (PhoshPomodoroQuickSetting *self) +{ + self->remaining = 0; + g_clear_handle_id (&self->update_id, g_source_remove); +} + + +static void +show_notification (PhoshPomodoroQuickSetting *self) +{ + PhoshNotifyManager *noti_manager = phosh_notify_manager_get_default (); + g_autoptr (PhoshNotification) noti = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GIcon) app_icon = NULL; + const char *summary; + g_autofree char *body = NULL; + + switch (self->state) { + case PHOSH_POMODORO_STATE_ACTIVE: + summary = _("Pomodoro start"); + body = g_strdup_printf (_("Focus on your task for %d minutes"), + g_settings_get_int (self->settings, "active-duration") / 60); + icon = g_themed_icon_new (ACTIVE_ICON); + break; + case PHOSH_POMODORO_STATE_BREAK: + summary = _("Take a break"); + /* Translators: Pomodoro is a technique, no need to translate it */ + body = g_strdup_printf (_("You have %d minutes until next Pomodoro"), + g_settings_get_int (self->settings, "break-duration") / 60); + icon = g_themed_icon_new (BREAK_ICON); + break; + case PHOSH_POMODORO_STATE_OFF: + default: + return; + } + + app_icon = g_themed_icon_new (ACTIVE_ICON); + noti = g_object_new (PHOSH_TYPE_NOTIFICATION, + "summary", summary, + "body", body, + "image", icon, + /* Translators: Pomodoro is a technique, no need to translate it */ + "app-name", _("Pomodoro Timer"), + "app-icon", app_icon, + NULL); + phosh_notify_manager_add_shell_notification (noti_manager, + noti, + 0, + PHOSH_NOTIFICATION_DEFAULT_TIMEOUT); +} + + +static void +update_label (PhoshPomodoroQuickSetting *self) +{ + g_autofree char *label = NULL; + + switch (self->state) { + case PHOSH_POMODORO_STATE_ACTIVE: + case PHOSH_POMODORO_STATE_BREAK: + label = cui_call_format_duration ((double) self->remaining); + break; + case PHOSH_POMODORO_STATE_OFF: + default: + /* Translators: Pomodoro is a technique, no need to translate it */ + label = g_strdup_printf (_("Pomodoro Off")); + } + + phosh_status_icon_set_info (self->info, label); +} + +static void phosh_pomodoro_quick_setting_set_state (PhoshPomodoroQuickSetting *self, + PhoshPomodoroState state); + +static gboolean +on_update_expired (gpointer user_data) +{ + PhoshPomodoroQuickSetting *self = PHOSH_POMODORO_QUICK_SETTING (user_data); + + self->remaining -= UPDATE_INTERVAL; + if (self->remaining > 0) { + update_label (self); + return G_SOURCE_CONTINUE; + } + + switch (self->state) { + case PHOSH_POMODORO_STATE_ACTIVE: + phosh_pomodoro_quick_setting_set_state (self, PHOSH_POMODORO_STATE_BREAK); + break; + case PHOSH_POMODORO_STATE_BREAK: + phosh_pomodoro_quick_setting_set_state (self, PHOSH_POMODORO_STATE_ACTIVE); + break; + case PHOSH_POMODORO_STATE_OFF: + default: + g_return_val_if_reached (G_SOURCE_REMOVE); + } + + update_label (self); + return G_SOURCE_REMOVE; +} + + +static void +phosh_pomodoro_quick_setting_set_state (PhoshPomodoroQuickSetting *self, PhoshPomodoroState state) +{ + int timeout = 0; + + if (self->state == state) + return; + self->state = state; + + phosh_pomodoro_quick_setting_clear_timers (self); + + switch (self->state) { + case PHOSH_POMODORO_STATE_ACTIVE: + timeout = g_settings_get_int (self->settings, "active-duration"); + break; + case PHOSH_POMODORO_STATE_BREAK: + timeout = g_settings_get_int (self->settings, "break-duration"); + break; + case PHOSH_POMODORO_STATE_OFF: + default: + break; + } + + if (timeout) { + self->remaining = timeout; + self->update_id = g_timeout_add_seconds (UPDATE_INTERVAL, on_update_expired, self); + } + + update_label (self); + show_notification (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); +} + + +static void +on_shell_locked (PhoshPomodoroQuickSetting *self, GParamSpec *pspec, PhoshShell *shell) +{ + g_assert (PHOSH_IS_SHELL (shell)); + g_assert (PHOSH_IS_POMODORO_QUICK_SETTING (self)); + + if (phosh_shell_get_locked (shell)) + return; + + if (self->state != PHOSH_POMODORO_STATE_OFF) + return; + + if (!g_settings_get_boolean (self->settings, START_ON_UNLOCK_KEY)) + return; + + phosh_pomodoro_quick_setting_set_state (self, PHOSH_POMODORO_STATE_ACTIVE); +} + + +static void +phosh_pomodoro_quick_setting_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshPomodoroQuickSetting *self = PHOSH_POMODORO_QUICK_SETTING (object); + + switch (property_id) { + case PROP_STATE: + phosh_pomodoro_quick_setting_set_state (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_pomodoro_quick_setting_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshPomodoroQuickSetting *self = PHOSH_POMODORO_QUICK_SETTING (object); + + switch (property_id) { + case PROP_STATE: + g_value_set_enum (value, self->state); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_clicked (PhoshPomodoroQuickSetting *self) +{ + PhoshPomodoroState new_state; + + switch (self->state) { + case PHOSH_POMODORO_STATE_OFF: + new_state = PHOSH_POMODORO_STATE_ACTIVE; + break; + case PHOSH_POMODORO_STATE_ACTIVE: + case PHOSH_POMODORO_STATE_BREAK: + default: + new_state = PHOSH_POMODORO_STATE_OFF; + break; + } + + phosh_pomodoro_quick_setting_set_state (self, new_state); +} + + +static gboolean +transform_to_active (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshPomodoroState state = g_value_get_enum (from_value); + gboolean active = FALSE; + + switch (state) { + case PHOSH_POMODORO_STATE_ACTIVE: + case PHOSH_POMODORO_STATE_BREAK: + active = TRUE; + break; + case PHOSH_POMODORO_STATE_OFF: + default: + break; + } + + g_value_set_boolean (to_value, active); + return TRUE; +} + + +static gboolean +transform_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshPomodoroState state = g_value_get_enum (from_value); + const char *icon_name; + + switch (state) { + case PHOSH_POMODORO_STATE_ACTIVE: + icon_name = ACTIVE_ICON; + break; + case PHOSH_POMODORO_STATE_BREAK: + icon_name = BREAK_ICON; + break; + case PHOSH_POMODORO_STATE_OFF: + default: + icon_name = "pomodoro-off-symbolic"; + break; + } + + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static void +phosh_pomodoro_quick_setting_finalize (GObject *gobject) +{ + PhoshPomodoroQuickSetting *self = PHOSH_POMODORO_QUICK_SETTING (gobject); + + phosh_pomodoro_quick_setting_clear_timers (self); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_pomodoro_quick_setting_parent_class)->finalize (gobject); +} + + +static void +phosh_pomodoro_quick_setting_class_init (PhoshPomodoroQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_pomodoro_quick_setting_finalize; + object_class->set_property = phosh_pomodoro_quick_setting_set_property; + object_class->get_property = phosh_pomodoro_quick_setting_get_property; + + props[PROP_STATE] = + g_param_spec_enum ("state", "", "", + PHOSH_TYPE_POMODORO_STATE, + PHOSH_POMODORO_STATE_OFF, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/pomodoro-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshPomodoroQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_pomodoro_quick_setting_init (PhoshPomodoroQuickSetting *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/pomodoro-quick-setting/icons"); + + self->settings = g_settings_new ("mobi.phosh.plugins.pomodoro"); + + g_object_bind_property_full (self, "state", + self, "active", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_active, + NULL, NULL, NULL); + + g_object_bind_property_full (self, "state", + self->info, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_icon_name, + NULL, NULL, NULL); + + g_signal_connect_object (phosh_shell_get_default (), + "notify::locked", + G_CALLBACK (on_shell_locked), + self, + G_CONNECT_SWAPPED); + if (g_settings_get_boolean (self->settings, START_ON_UNLOCK_KEY) && !_started_on_login) { + phosh_pomodoro_quick_setting_set_state (self, PHOSH_POMODORO_STATE_ACTIVE); + _started_on_login = TRUE; + } + + update_label (self); +} diff --git a/plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in new file mode 100644 index 000000000..59238a3d2 --- /dev/null +++ b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in @@ -0,0 +1,13 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +# Translators: "Pomodoro" is a technique, no need to translate it +Name=Pomodoro Quick Setting +Types=quick-setting; +Comment=Simple Pomodoro Timer +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=pomodoro-quick-setting + +[Prefs] +Id=@name@-prefs +Plugin=@plugin_prefs_dir@/libphosh-plugin-prefs-@name@.so diff --git a/plugins/pomodoro-quick-setting/pomodoro-quick-setting.h b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.h new file mode 100644 index 000000000..9af844d0d --- /dev/null +++ b/plugins/pomodoro-quick-setting/pomodoro-quick-setting.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +typedef enum _PhoshPomodoroState { + PHOSH_POMODORO_STATE_OFF = 0, + PHOSH_POMODORO_STATE_ACTIVE = 1, + PHOSH_POMODORO_STATE_BREAK = 2, +} PhoshPomodoroState; + +#define PHOSH_TYPE_POMODORO_QUICK_SETTING phosh_pomodoro_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshPomodoroQuickSetting, + phosh_pomodoro_quick_setting, + PHOSH, POMODORO_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/pomodoro-quick-setting/prefs/meson.build b/plugins/pomodoro-quick-setting/prefs/meson.build new file mode 100644 index 000000000..c63728123 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/meson.build @@ -0,0 +1,25 @@ +pomodoro_quick_setting_prefs_resources = gnome.compile_resources( + 'phosh-plugin-prefs-pomodoro-quick-setting-resources', + 'phosh-plugin-prefs-pomodoro-quick-setting.gresources.xml', + c_name: 'phosh_plugin_prefs_pomodoro_quick_setting', +) + +pomodoro_quick_setting_plugin_prefs_sources = files( + 'phosh-plugin-prefs-pomodoro-quick-setting.c', + 'pomodoro-quick-setting-prefs.c', + 'pomodoro-quick-setting-prefs.h', +) + +phosh_pomodoro_quick_setting_plugin_prefs = shared_module( + 'phosh-plugin-prefs-pomodoro-quick-setting', + pomodoro_quick_setting_plugin_prefs_sources, + pomodoro_quick_setting_prefs_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-prefs-pomodoro-quick-setting"', + '-DPLUGIN_PREFS_NAME="@0@-prefs"'.format(name), + '-DADW_VERSION_MIN_REQUIRED=@0@'.format(adw_ver_str), + ], + dependencies: plugin_prefs_dep, + install: true, + install_dir: plugin_prefs_dir, +) diff --git a/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.c b/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.c new file mode 100644 index 000000000..477cb7ff7 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.c @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "phosh-plugin-prefs-config.h" + +#include "pomodoro-quick-setting-prefs.h" + +#include "phosh-plugin.h" + +#include + +char **g_io_phosh_plugin_prefs_pomodoro_quick_setting_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS, + PHOSH_TYPE_POMODORO_QUICK_SETTING_PREFS, + PLUGIN_PREFS_NAME, + 10); + + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_prefs_pomodoro_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET_PREFS, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.gresources.xml b/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.gresources.xml new file mode 100644 index 000000000..67de062f0 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/phosh-plugin-prefs-pomodoro-quick-setting.gresources.xml @@ -0,0 +1,6 @@ + + + + prefs.ui + + diff --git a/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.c b/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.c new file mode 100644 index 000000000..55765c3b0 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.c @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-plugin-prefs-config.h" + +#include "pomodoro-quick-setting-prefs.h" + +#include + +#include + +#define POMODORO_QUICK_SETTING_SCHEMA_ID "mobi.phosh.plugins.pomodoro" +#define ACTIVE_DURATION_KEY "active-duration" +#define BREAK_DURATION_KEY "break-duration" +#define START_ON_UNLOCK_KEY "start-on-unlock" + +/** + * PhoshPomodoroQuickSettingPrefs: + * + * Preferences for Pomodoro quick setting plugin + */ +struct _PhoshPomodoroQuickSettingPrefs { + AdwPreferencesDialog parent; + + AdwSpinRow *active_duration_spin_row; + AdwSpinRow *break_duration_spin_row; + AdwSwitchRow *start_on_unlock_switch_row; + + GSettings *setting; +}; + +G_DEFINE_TYPE (PhoshPomodoroQuickSettingPrefs, phosh_pomodoro_quick_setting_prefs, + ADW_TYPE_PREFERENCES_DIALOG); + + +static gboolean +duration_get_mapping (GValue *value, GVariant *variant, gpointer user_data) +{ + float duration; + + duration = g_variant_get_int32 (variant) / 60.0; + g_value_set_double (value, duration); + + return TRUE; +} + + +static GVariant * +duration_set_mapping (const GValue *value, const GVariantType *type, gpointer user_data) +{ + double duration; + + duration = g_value_get_double (value) * 60.0; + + return g_variant_new_int32 ((gint32)rint (duration)); +} + + +static void +phosh_pomodoro_quick_setting_prefs_finalize (GObject *object) +{ + PhoshPomodoroQuickSettingPrefs *self = PHOSH_POMODORO_QUICK_SETTING_PREFS (object); + + g_clear_object (&self->setting); + + G_OBJECT_CLASS (phosh_pomodoro_quick_setting_prefs_parent_class)->finalize (object); +} + + +static void +phosh_pomodoro_quick_setting_prefs_class_init (PhoshPomodoroQuickSettingPrefsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_pomodoro_quick_setting_prefs_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/" + "pomodoro-quick-setting-prefs/prefs.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshPomodoroQuickSettingPrefs, + active_duration_spin_row); + gtk_widget_class_bind_template_child (widget_class, PhoshPomodoroQuickSettingPrefs, + break_duration_spin_row); + gtk_widget_class_bind_template_child (widget_class, PhoshPomodoroQuickSettingPrefs, + start_on_unlock_switch_row); +} + + +static void +phosh_pomodoro_quick_setting_prefs_init (PhoshPomodoroQuickSettingPrefs *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->setting = g_settings_new (POMODORO_QUICK_SETTING_SCHEMA_ID); + + g_settings_bind_with_mapping (self->setting, ACTIVE_DURATION_KEY, + self->active_duration_spin_row, "value", + G_SETTINGS_BIND_DEFAULT, + duration_get_mapping, + duration_set_mapping, + NULL /* userdata */, + NULL /* destroyfunc */); + + g_settings_bind_with_mapping (self->setting, BREAK_DURATION_KEY, + self->break_duration_spin_row, "value", + G_SETTINGS_BIND_DEFAULT, + duration_get_mapping, + duration_set_mapping, + NULL /* userdata */, + NULL /* destroyfunc */); + + g_settings_bind (self->setting, START_ON_UNLOCK_KEY, + self->start_on_unlock_switch_row, "active", + G_SETTINGS_BIND_DEFAULT); +} diff --git a/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.h b/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.h new file mode 100644 index 000000000..218c62c26 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/pomodoro-quick-setting-prefs.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_POMODORO_QUICK_SETTING_PREFS (phosh_pomodoro_quick_setting_prefs_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshPomodoroQuickSettingPrefs, + phosh_pomodoro_quick_setting_prefs, + PHOSH, POMODORO_QUICK_SETTING_PREFS, AdwPreferencesDialog) + +G_END_DECLS diff --git a/plugins/pomodoro-quick-setting/prefs/prefs.ui b/plugins/pomodoro-quick-setting/prefs/prefs.ui new file mode 100644 index 000000000..78454acb9 --- /dev/null +++ b/plugins/pomodoro-quick-setting/prefs/prefs.ui @@ -0,0 +1,54 @@ + + + + + + diff --git a/plugins/pomodoro-quick-setting/qs.ui b/plugins/pomodoro-quick-setting/qs.ui new file mode 100644 index 000000000..b1e400fa3 --- /dev/null +++ b/plugins/pomodoro-quick-setting/qs.ui @@ -0,0 +1,12 @@ + + + + + + 1 + 16 + + diff --git a/plugins/scaling-quick-setting/icons/screen-scaling-large-symbolic.svg b/plugins/scaling-quick-setting/icons/screen-scaling-large-symbolic.svg new file mode 100644 index 000000000..13599cb21 --- /dev/null +++ b/plugins/scaling-quick-setting/icons/screen-scaling-large-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/scaling-quick-setting/icons/screen-scaling-small-symbolic.svg b/plugins/scaling-quick-setting/icons/screen-scaling-small-symbolic.svg new file mode 100644 index 000000000..ccf262462 --- /dev/null +++ b/plugins/scaling-quick-setting/icons/screen-scaling-small-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/scaling-quick-setting/meson.build b/plugins/scaling-quick-setting/meson.build new file mode 100644 index 000000000..06475d2b1 --- /dev/null +++ b/plugins/scaling-quick-setting/meson.build @@ -0,0 +1,49 @@ +name = 'scaling-quick-setting' + +scaling_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-scaling-quick-setting-resources', + 'phosh-plugin-scaling-quick-setting.gresources.xml', + c_name: 'phosh_plugin_scaling_quick_setting', +) + +scaling_quick_setting_plugin_sources = files( + 'phosh-plugin-scaling-quick-setting.c', + 'scale-row.c', + 'scaling-quick-setting.c', +) + +phosh_scaling_quick_setting_plugin = shared_module( + 'phosh-plugin-scaling-quick-setting', + scaling_quick_setting_plugin_sources, + scaling_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data( + 'icons/screen-scaling-small-symbolic.svg', + rename: 'scaling-quick-setting-symbolic.svg', + install_dir: phosh_plugin_icon_dir, +) diff --git a/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.c b/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.c new file mode 100644 index 000000000..be96154b0 --- /dev/null +++ b/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Adam Honse + */ + +#include "phosh-plugin.h" +#include "scaling-quick-setting.h" + + +char **g_io_phosh_plugin_scaling_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_SCALING_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_scaling_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.gresources.xml b/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.gresources.xml new file mode 100644 index 000000000..b9f791495 --- /dev/null +++ b/plugins/scaling-quick-setting/phosh-plugin-scaling-quick-setting.gresources.xml @@ -0,0 +1,9 @@ + + + + qs.ui + scale-row.ui + icons/screen-scaling-small-symbolic.svg + icons/screen-scaling-large-symbolic.svg + + diff --git a/plugins/scaling-quick-setting/qs.ui b/plugins/scaling-quick-setting/qs.ui new file mode 100644 index 000000000..21f77c4e4 --- /dev/null +++ b/plugins/scaling-quick-setting/qs.ui @@ -0,0 +1,25 @@ + + + + + + 1 + + + 1 + Monitor scales + list_box + + + 1 + none + + + + diff --git a/plugins/scaling-quick-setting/scale-row.c b/plugins/scaling-quick-setting/scale-row.c new file mode 100644 index 000000000..a2963521f --- /dev/null +++ b/plugins/scaling-quick-setting/scale-row.c @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "scale-row.h" + +#include + +#include + +/** + * PhoshScaleRow: + * + * A widget to display a single monitor scale + */ + +enum { + PROP_0, + PROP_SCALE, + PROP_SELECTED, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshScaleRow { + HdyActionRow parent; + + GtkRevealer *revealer; + double scale; + gboolean selected; +}; + +G_DEFINE_TYPE (PhoshScaleRow, phosh_scale_row, HDY_TYPE_ACTION_ROW); + + +static void +phosh_scale_row_set_device (PhoshScaleRow *self, double scale) +{ + g_autofree char *label = NULL; + + self->scale = scale; + + /* Translators: This is scale factor of a monitor in percent */ + label = g_strdup_printf (_("%d%%"), (int)round((scale * 100))); + + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), label); + + g_object_bind_property (self, + "selected", + self->revealer, + "reveal-child", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +} + + +static void +set_selected (PhoshScaleRow *self, gboolean selected) +{ + if (self->selected == selected) + return; + + self->selected = selected; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED]); +} + + +static void +phosh_scale_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshScaleRow *self = PHOSH_SCALE_ROW (object); + + switch (property_id) { + case PROP_SCALE: + phosh_scale_row_set_device (self, g_value_get_double (value)); + break; + case PROP_SELECTED: + set_selected (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_scale_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshScaleRow *self = PHOSH_SCALE_ROW (object); + + switch (property_id) { + case PROP_SELECTED: + g_value_set_boolean (value, self->selected); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_scale_row_class_init (PhoshScaleRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_scale_row_get_property; + object_class->set_property = phosh_scale_row_set_property; + + /** + * PhoshScaleRow:scale: + * + * A monitor scale + */ + props[PROP_SCALE] = + g_param_spec_double ("scale", "", "", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshScaleRow:selected: + * + * Whether this is the current selected monitor scale + */ + props[PROP_SELECTED] = + g_param_spec_boolean ("selected", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/" + "scaling-quick-setting/scale-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshScaleRow, revealer); +} + + +static void +phosh_scale_row_init (PhoshScaleRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_scale_row_new (double scale, gboolean selected) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_SCALE_ROW, + "scale", scale, + "selected", selected, + NULL)); +} + + +double +phosh_scale_row_get_scale (PhoshScaleRow *self) +{ + g_return_val_if_fail (PHOSH_IS_SCALE_ROW (self), 1.0); + + return self->scale; +} diff --git a/plugins/scaling-quick-setting/scale-row.h b/plugins/scaling-quick-setting/scale-row.h new file mode 100644 index 000000000..8c8f78e62 --- /dev/null +++ b/plugins/scaling-quick-setting/scale-row.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SCALE_ROW phosh_scale_row_get_type () + +G_DECLARE_FINAL_TYPE (PhoshScaleRow, phosh_scale_row, PHOSH, SCALE_ROW, HdyActionRow) + +GtkWidget *phosh_scale_row_new (double scale, gboolean selected); +double phosh_scale_row_get_scale (PhoshScaleRow *self); + +G_END_DECLS diff --git a/plugins/scaling-quick-setting/scale-row.ui b/plugins/scaling-quick-setting/scale-row.ui new file mode 100644 index 000000000..513344e83 --- /dev/null +++ b/plugins/scaling-quick-setting/scale-row.ui @@ -0,0 +1,27 @@ + + + + + + diff --git a/plugins/scaling-quick-setting/scaling-quick-setting.c b/plugins/scaling-quick-setting/scaling-quick-setting.c new file mode 100644 index 000000000..a8d0758ab --- /dev/null +++ b/plugins/scaling-quick-setting/scaling-quick-setting.c @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Adam Honse + * Guido Günther + */ + +#include "scaling-quick-setting.h" +#include "plugin-shell.h" +#include "scale-row.h" +#include "util.h" + +#include + +/** + * PhoshScalingQuickSetting: + * + * A quick setting for adjusting display scaling. + */ + +struct _PhoshScalingQuickSetting { + PhoshQuickSetting parent; + + GtkListBox *list_box; + PhoshStatusPage *status_page; + + double scale; + double last_scale; + PhoshStatusIcon *info; + PhoshMonitor *monitor; + guint current_mode; +}; + +G_DEFINE_TYPE (PhoshScalingQuickSetting, phosh_scaling_quick_setting, PHOSH_TYPE_QUICK_SETTING); + + +static gboolean +transform_to_sensitive (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + guint n_monitors = g_value_get_int (from_value); + + g_value_set_boolean (to_value, n_monitors == 1); + return TRUE; +} + + +static void +fill_scales (PhoshScalingQuickSetting *self, guint32 width, guint32 height) +{ + int n_scales; + g_autoptr (GList) children = NULL; + g_autofree float *scales = NULL; + + scales = phosh_util_calculate_supported_mode_scales (width, height, &n_scales, TRUE); + + children = gtk_container_get_children (GTK_CONTAINER (self->list_box)); + for (GList *child = children; child; child = child->next) + gtk_container_remove (GTK_CONTAINER (self->list_box), child->data); + + for (int i = 0; i < n_scales; i++) { + gboolean selected = G_APPROX_VALUE (self->scale, scales[i], FLT_EPSILON); + GtkWidget *row = phosh_scale_row_new (scales[i], selected); + + gtk_list_box_insert (self->list_box, row, -1); + } +} + + +static void +on_monitor_configured (PhoshScalingQuickSetting *self, PhoshMonitor *monitor) +{ + double scale; + + g_assert (PHOSH_IS_SCALING_QUICK_SETTING (self)); + g_assert (PHOSH_IS_MONITOR (monitor)); + + scale = phosh_monitor_get_fractional_scale (monitor); + if (!G_APPROX_VALUE (self->scale, scale, FLT_EPSILON)) { + const char *icon_name; + g_autofree char *label = NULL; + + self->last_scale = self->scale; + self->scale = scale; + + /* Translators: This is scale factor of a monitor in percent */ + label = g_strdup_printf (_("%d%%"), (int)round(scale * 100)); + phosh_status_icon_set_info (self->info, label); + + icon_name = self->scale < 2.0 ? "screen-scaling-small-symbolic" : + "screen-scaling-large-symbolic"; + phosh_status_icon_set_icon_name (self->info, icon_name); + } + + fill_scales (self, monitor->width, monitor->height); +} + + +static void +set_primary_monitor (PhoshScalingQuickSetting *self, PhoshMonitor *monitor) +{ + g_assert (PHOSH_IS_SCALING_QUICK_SETTING (self)); + g_assert (PHOSH_IS_MONITOR (monitor)); + + if (self->monitor == monitor) + return; + + if (self->monitor) { + g_signal_handlers_disconnect_by_data (self->monitor, self); + g_object_remove_weak_pointer (G_OBJECT (self->monitor), (gpointer *)&self->monitor); + self->current_mode = -1; + self->last_scale = 1.0; + } + + self->monitor = monitor; + + if (self->monitor) { + g_object_add_weak_pointer (G_OBJECT (self->monitor), (gpointer *)&self->monitor); + g_signal_connect_object (self->monitor, + "configured", + G_CALLBACK (on_monitor_configured), + self, + G_CONNECT_SWAPPED); + } +} + + +static void +on_primary_monitor_changed (PhoshScalingQuickSetting *self, GParamSpec *pspec, PhoshShell *shell) +{ + set_primary_monitor (self, phosh_shell_get_primary_monitor (shell)); +} + + +static void +set_scale (PhoshScalingQuickSetting *self, double scale) +{ + PhoshMonitorManager *monitor_manager; + + monitor_manager = phosh_shell_get_monitor_manager (phosh_shell_get_default ()); + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (monitor_manager)); + g_return_if_fail (PHOSH_IS_MONITOR (self->monitor)); + g_return_if_fail (scale >= 0.0); + + g_debug ("Setting monior scale to %f", scale); + + phosh_monitor_manager_set_monitor_scale (monitor_manager, self->monitor, scale); + phosh_monitor_manager_apply_monitor_config (monitor_manager); +} + + +static void +on_clicked (PhoshScalingQuickSetting *self) +{ + double scale; + + scale = self->last_scale >= 1.0 ? self->last_scale : 1.0; + set_scale (self, scale); +} + + +static void +on_scale_row_activated (PhoshScalingQuickSetting *self, GtkListBoxRow *row) +{ + PhoshScaleRow *scale_row = PHOSH_SCALE_ROW (row); + double scale; + + scale = phosh_scale_row_get_scale (scale_row); + set_scale (self, scale); + g_signal_emit_by_name (self->status_page, "done", TRUE); +} + + +static void +phosh_scaling_quick_setting_class_init (PhoshScalingQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/scaling-quick-setting/icons"); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/scaling-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshScalingQuickSetting, info); + gtk_widget_class_bind_template_child (widget_class, PhoshScalingQuickSetting, list_box); + gtk_widget_class_bind_template_child (widget_class, PhoshScalingQuickSetting, status_page); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_scale_row_activated); +} + + +static void +phosh_scaling_quick_setting_init (PhoshScalingQuickSetting *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitorManager *monitor_manager; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->last_scale = 1.0; + self->current_mode = -1; + + monitor_manager = phosh_shell_get_monitor_manager (shell); + g_object_bind_property_full (monitor_manager, "n-monitors", + self, "sensitive", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_to_sensitive, + NULL, NULL, NULL); + + g_signal_connect_object (shell, + "notify::primary-monitor", + G_CALLBACK (on_primary_monitor_changed), + self, + G_CONNECT_SWAPPED); + + /* Catch up with current monitor */ + on_primary_monitor_changed (self, NULL, shell); + if (self->monitor) + on_monitor_configured (self, self->monitor); +} diff --git a/plugins/scaling-quick-setting/scaling-quick-setting.desktop.in.in b/plugins/scaling-quick-setting/scaling-quick-setting.desktop.in.in new file mode 100644 index 000000000..e7887714e --- /dev/null +++ b/plugins/scaling-quick-setting/scaling-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Scaling Quick Setting +Types=quick-setting; +Comment=A quick setting for adjusting the display scaling. +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=scaling-quick-setting diff --git a/plugins/scaling-quick-setting/scaling-quick-setting.h b/plugins/scaling-quick-setting/scaling-quick-setting.h new file mode 100644 index 000000000..3dfd23663 --- /dev/null +++ b/plugins/scaling-quick-setting/scaling-quick-setting.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Adam Honse + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SCALING_QUICK_SETTING phosh_scaling_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshScalingQuickSetting, phosh_scaling_quick_setting, PHOSH, + SCALING_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/simple-custom-quick-setting/meson.build b/plugins/simple-custom-quick-setting/meson.build new file mode 100644 index 000000000..442ef308a --- /dev/null +++ b/plugins/simple-custom-quick-setting/meson.build @@ -0,0 +1,42 @@ +name = 'simple-custom-quick-setting' + +simple_custom_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-simple-custom-quick-setting-resources', + 'phosh-plugin-simple-custom-quick-setting.gresources.xml', + c_name: 'phosh_plugin_simple_custom_quick_setting', +) + +simple_custom_quick_setting_plugin_sources = files( + 'phosh-plugin-simple-custom-quick-setting.c', + 'simple-custom-quick-setting.c', +) + +phosh_simple_custom_quick_setting_plugin = shared_module( + 'phosh-plugin-simple-custom-quick-setting', + simple_custom_quick_setting_plugin_sources, + simple_custom_quick_setting_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: plugin_dep, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) diff --git a/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.c b/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.c new file mode 100644 index 000000000..6236ca62c --- /dev/null +++ b/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "phosh-plugin.h" +#include "simple-custom-quick-setting.h" + + +char **g_io_phosh_plugin_simple_custom_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_SIMPLE_CUSTOM_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_simple_custom_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.gresources.xml b/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.gresources.xml new file mode 100644 index 000000000..b3f5e11c8 --- /dev/null +++ b/plugins/simple-custom-quick-setting/phosh-plugin-simple-custom-quick-setting.gresources.xml @@ -0,0 +1,6 @@ + + + + qs.ui + + diff --git a/plugins/simple-custom-quick-setting/qs.ui b/plugins/simple-custom-quick-setting/qs.ui new file mode 100644 index 000000000..55ba7ec49 --- /dev/null +++ b/plugins/simple-custom-quick-setting/qs.ui @@ -0,0 +1,40 @@ + + + + + + 1 + 16 + + + 1 + Simple Custom Quick Setting + placeholder + footer + + + 1 + face-angel-symbolic + label + + + 1 + Have a nice day! + + + 1 + 1 + + + + 1 + end + Open Plugin Settings + + + + diff --git a/plugins/simple-custom-quick-setting/simple-custom-quick-setting.c b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.c new file mode 100644 index 000000000..2becd31eb --- /dev/null +++ b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.c @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "simple-custom-quick-setting.h" + +#include + +/** + * PhoshSimpleCustomQuickSetting: + * + * A simple custom quick setting for demonstration purposes. + */ + +struct _PhoshSimpleCustomQuickSetting { + PhoshQuickSetting parent; + + PhoshStatusIcon *info; +}; + +G_DEFINE_TYPE (PhoshSimpleCustomQuickSetting, phosh_simple_custom_quick_setting, PHOSH_TYPE_QUICK_SETTING); + + +static void +on_clicked (PhoshSimpleCustomQuickSetting *self) +{ + gboolean active = phosh_quick_setting_get_active (PHOSH_QUICK_SETTING (self)); + + if (active) { + phosh_status_icon_set_icon_name (self->info, "face-shutmouth-symbolic"); + phosh_status_icon_set_info (self->info, _("I'm Inactive")); + } else { + phosh_status_icon_set_icon_name (self->info, "face-smile-big-symbolic"); + phosh_status_icon_set_info (self->info, _("I'm Active")); + } + + phosh_quick_setting_set_active (PHOSH_QUICK_SETTING (self), !active); +} + + +static void +on_footer_clicked (PhoshSimpleCustomQuickSetting *self) +{ + g_message ("Footer clicked; open settings panel of the plugin"); +} + + +static void +phosh_simple_custom_quick_setting_class_init (PhoshSimpleCustomQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/simple-custom-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshSimpleCustomQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_footer_clicked); +} + + +static void +phosh_simple_custom_quick_setting_init (PhoshSimpleCustomQuickSetting *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + on_clicked (self); +} diff --git a/plugins/simple-custom-quick-setting/simple-custom-quick-setting.desktop.in.in b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.desktop.in.in new file mode 100644 index 000000000..f7e2f0dab --- /dev/null +++ b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Simple Custom Quick Setting +Types=quick-setting; +Comment=A simple custom quick setting for demonstration purposes. +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +NoDisplay=true \ No newline at end of file diff --git a/plugins/simple-custom-quick-setting/simple-custom-quick-setting.h b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.h new file mode 100644 index 000000000..d461b503d --- /dev/null +++ b/plugins/simple-custom-quick-setting/simple-custom-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SIMPLE_CUSTOM_QUICK_SETTING phosh_simple_custom_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshSimpleCustomQuickSetting, + phosh_simple_custom_quick_setting, + PHOSH, SIMPLE_CUSTOM_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/ticket-box/meson.build b/plugins/ticket-box/meson.build new file mode 100644 index 000000000..b467fde92 --- /dev/null +++ b/plugins/ticket-box/meson.build @@ -0,0 +1,69 @@ +name = 'ticket-box' + +ticket_box_plugin_deps = [ + plugin_dep, + gio_unix_dep, + dependency('evince-document-3.0'), + dependency('evince-view-3.0'), +] + +ticket_box_resources = gnome.compile_resources( + 'phosh-plugin-ticket-box-resources', + 'phosh-plugin-ticket-box.gresources.xml', + c_name: 'phosh_plugin_ticket_box', +) + +ticket_box_plugin_sources = files( + 'phosh-plugin-ticket-box.c', + 'ticket-box.c', + 'ticket-box.h', + 'ticket-row.c', + 'ticket-row.h', + 'ticket.c', + 'ticket.h', +) + +phosh_ticket_box_plugin = shared_module( + 'phosh-plugin-ticket-box', + ticket_box_plugin_sources, + ticket_box_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: ticket_box_plugin_deps, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +ticket_box_schema = 'sm.puri.phosh.plugins.ticket-box.gschema.xml' +compiled = gnome.compile_schemas(depend_files: ticket_box_schema) +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test( + 'Validate @0@ schema file'.format(ticket_box_schema), + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()], + ) +endif +install_data(ticket_box_schema, install_dir: 'share/glib-2.0/schemas') + +subdir('prefs') diff --git a/plugins/ticket-box/phosh-plugin-ticket-box.c b/plugins/ticket-box/phosh-plugin-ticket-box.c new file mode 100644 index 000000000..433761a80 --- /dev/null +++ b/plugins/ticket-box/phosh-plugin-ticket-box.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "ticket-box.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_ticket_box_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_TICKET_BOX, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_ticket_box_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/ticket-box/phosh-plugin-ticket-box.gresources.xml b/plugins/ticket-box/phosh-plugin-ticket-box.gresources.xml new file mode 100644 index 000000000..112c261d4 --- /dev/null +++ b/plugins/ticket-box/phosh-plugin-ticket-box.gresources.xml @@ -0,0 +1,7 @@ + + + + ticket-box.ui + stylesheet/common.css + + diff --git a/plugins/ticket-box/prefs/meson.build b/plugins/ticket-box/prefs/meson.build new file mode 100644 index 000000000..b3e3936b7 --- /dev/null +++ b/plugins/ticket-box/prefs/meson.build @@ -0,0 +1,25 @@ +ticket_box_prefs_resources = gnome.compile_resources( + 'phosh-plugin-prefs-ticket-box-resources', + 'phosh-plugin-prefs-ticket-box.gresources.xml', + c_name: 'phosh_plugin_prefs_ticket_box', +) + +ticket_box_plugin_prefs_sources = files( + 'phosh-plugin-prefs-ticket-box.c', + 'ticket-box-prefs.c', + 'ticket-box-prefs.h', +) + +phosh_ticket_box_plugin_prefs = shared_module( + 'phosh-plugin-prefs-ticket-box', + ticket_box_plugin_prefs_sources, + ticket_box_prefs_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-prefs-ticket-box"', + '-DPLUGIN_PREFS_NAME="@0@-prefs"'.format(name), + '-DADW_VERSION_MIN_REQUIRED=@0@'.format(adw_ver_str), + ], + dependencies: plugin_prefs_dep, + install: true, + install_dir: plugin_prefs_dir, +) diff --git a/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.c b/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.c new file mode 100644 index 000000000..03ae53bbd --- /dev/null +++ b/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.c @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2022 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "phosh-plugin-prefs-config.h" + +#include "ticket-box-prefs.h" + +#include "phosh-plugin.h" + +#include + +char **g_io_phosh_plugin_prefs_ticket_box_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, + PHOSH_TYPE_TICKET_BOX_PREFS, + PLUGIN_PREFS_NAME, + 10); + + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_prefs_ticket_box_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.gresources.xml b/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.gresources.xml new file mode 100644 index 000000000..4aedbf006 --- /dev/null +++ b/plugins/ticket-box/prefs/phosh-plugin-prefs-ticket-box.gresources.xml @@ -0,0 +1,6 @@ + + + + ticket-box-prefs.ui + + diff --git a/plugins/ticket-box/prefs/ticket-box-prefs.c b/plugins/ticket-box/prefs/ticket-box-prefs.c new file mode 100644 index 000000000..c95b2a38c --- /dev/null +++ b/plugins/ticket-box/prefs/ticket-box-prefs.c @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2022 Purism SPC + * 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-plugin-prefs-config.h" + +#include "ticket-box-prefs.h" + +#define TICKET_BOX_SCHEMA_ID "sm.puri.phosh.plugins.ticket-box" +#define TICKET_BOX_FOLDER_KEY "folder" + +#include + +/** + * PhoshTicketBoxPrefs: + * + * Preferences for ticket-box plugin + */ +struct _PhoshTicketBoxPrefs { + AdwPreferencesDialog parent; + + GtkWidget *folder_entry; + GtkWidget *folder_button; + + char *ticket_box_path; + GSettings *settings; +}; + +G_DEFINE_TYPE (PhoshTicketBoxPrefs, phosh_ticket_box_prefs, ADW_TYPE_PREFERENCES_DIALOG); + +static gboolean +folder_get_mapping (GValue *value, GVariant *variant, gpointer user_data) +{ + g_autofree char *path = NULL; + const char *folder; + + folder = g_variant_get_string (variant, NULL); + + if (folder[0] != '/') + path = g_build_filename (g_get_home_dir (), folder, NULL); + else + path = g_strdup (folder); + + g_value_set_string (value, path); + + return TRUE; +} + + +static void +on_select_folder_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshTicketBoxPrefs *self = NULL; + g_autoptr (GError) err = NULL; + g_autoptr (GFile) file = NULL; + g_autofree char *filename = NULL; + + file = gtk_file_dialog_select_folder_finish (GTK_FILE_DIALOG (source_object), + res, + &err); + if (!file) { + if (!g_error_matches (err, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED)) + g_warning ("Failed to folders: %s", err->message); + return; + } + + self = PHOSH_TICKET_BOX_PREFS (user_data); + g_assert (PHOSH_IS_TICKET_BOX_PREFS (self)); + + filename = g_file_get_path (file); + gtk_editable_set_text (GTK_EDITABLE (self->folder_entry), filename); +} + + +static void +on_folder_button_clicked (PhoshTicketBoxPrefs *self) +{ + g_autoptr (GtkFileDialog) file_dialog = NULL; + const char *current; + g_autoptr (GFile) current_file = NULL; + GtkWindow *window; + + g_assert (PHOSH_IS_TICKET_BOX_PREFS (self)); + + current = gtk_editable_get_text (GTK_EDITABLE (self->folder_entry)); + current_file = g_file_new_for_path (current); + + file_dialog = g_object_new (GTK_TYPE_FILE_DIALOG, + "accept-label", _("_Open"), + "title", _("Choose Folder"), + "initial-file", current_file, + "modal", TRUE, + NULL); + + window = GTK_WINDOW (gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_WINDOW)); + gtk_file_dialog_select_folder (file_dialog, + window, + NULL, + on_select_folder_ready, + self); +} + + +static void +phosh_ticket_box_prefs_finalize (GObject *object) +{ + PhoshTicketBoxPrefs *self = PHOSH_TICKET_BOX_PREFS (object); + + g_clear_object (&self->settings); + g_clear_pointer (&self->ticket_box_path, g_free); + + G_OBJECT_CLASS (phosh_ticket_box_prefs_parent_class)->finalize (object); +} + +static void +phosh_ticket_box_prefs_class_init (PhoshTicketBoxPrefsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_ticket_box_prefs_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/ticket-box-prefs/ticket-box-prefs.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshTicketBoxPrefs, folder_entry); + gtk_widget_class_bind_template_callback (widget_class, on_folder_button_clicked); +} + + +static void +phosh_ticket_box_prefs_init (PhoshTicketBoxPrefs *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (TICKET_BOX_SCHEMA_ID); + g_settings_bind_with_mapping (self->settings, TICKET_BOX_FOLDER_KEY, + self->folder_entry, "text", + G_SETTINGS_BIND_DEFAULT, + folder_get_mapping, + NULL /* set_mapping */, + NULL /* userdata */, + NULL /* destroyfunc */); +} diff --git a/plugins/ticket-box/prefs/ticket-box-prefs.h b/plugins/ticket-box/prefs/ticket-box-prefs.h new file mode 100644 index 000000000..0be67ae3d --- /dev/null +++ b/plugins/ticket-box/prefs/ticket-box-prefs.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2022 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TICKET_BOX_PREFS (phosh_ticket_box_prefs_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshTicketBoxPrefs, phosh_ticket_box_prefs, PHOSH, TICKET_BOX_PREFS, AdwPreferencesDialog) + +G_END_DECLS diff --git a/plugins/ticket-box/prefs/ticket-box-prefs.ui b/plugins/ticket-box/prefs/ticket-box-prefs.ui new file mode 100644 index 000000000..919098bdf --- /dev/null +++ b/plugins/ticket-box/prefs/ticket-box-prefs.ui @@ -0,0 +1,44 @@ + + + + + + diff --git a/plugins/ticket-box/sm.puri.phosh.plugins.ticket-box.gschema.xml b/plugins/ticket-box/sm.puri.phosh.plugins.ticket-box.gschema.xml new file mode 100644 index 000000000..1342c1c32 --- /dev/null +++ b/plugins/ticket-box/sm.puri.phosh.plugins.ticket-box.gschema.xml @@ -0,0 +1,13 @@ + + + + 'phosh-ticket-box' + Folder containing tickets + + The folder that contains the tickets that should be available + on the lock screen. + + + + diff --git a/plugins/ticket-box/stylesheet/common.css b/plugins/ticket-box/stylesheet/common.css new file mode 100644 index 000000000..d564b64cd --- /dev/null +++ b/plugins/ticket-box/stylesheet/common.css @@ -0,0 +1,9 @@ +phosh-ticket-box { + background-color: @phosh_notification_bg_color; + border-radius: 12px; + padding: 8px; +} + +phosh-ticket-row { + margin-top: 6px; +} diff --git a/plugins/ticket-box/ticket-box.c b/plugins/ticket-box/ticket-box.c new file mode 100644 index 000000000..636c61711 --- /dev/null +++ b/plugins/ticket-box/ticket-box.c @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "ticket.h" +#include "ticket-box.h" +#include "ticket-row.h" + +#include +#include + +#define TICKET_BOX_SCHEMA_ID "sm.puri.phosh.plugins.ticket-box" +#define TICKET_BOX_FOLDER_KEY "folder" + +/** + * PhoshTicketBox + * + * Show tickets in a folder. For now we do PDF but + * should add PNG and pass. + */ +struct _PhoshTicketBox { + GtkBox parent; + + GFileMonitor *monitor; + GFile *dir; + char *ticket_box_path; + GCancellable *cancel; + + GListStore *model; + GtkListBox *lb_tickets; + GtkStack *stack_tickets; + + EvView *view; +}; + +G_DEFINE_TYPE (PhoshTicketBox, phosh_ticket_box, GTK_TYPE_BOX); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (EvDocument, g_object_unref) + +static void +on_row_selected (PhoshTicketBox *self, + GtkListBoxRow *row, + GtkListBox *box) +{ + g_autoptr (GError) err = NULL; + g_autoptr (PhoshTicket) ticket = NULL; + g_autoptr (EvDocument) doc = NULL; + g_autoptr (EvDocumentModel) model = NULL; + + if (row == NULL) + return; + + g_object_get (row, "ticket", &ticket, NULL); + g_debug ("row selected: %s", phosh_ticket_get_display_name (ticket)); + + doc = ev_document_factory_get_document_for_gfile (phosh_ticket_get_file (ticket), + EV_DOCUMENT_LOAD_FLAG_NONE, + self->cancel, + &err); + if (doc == NULL) { + g_warning ("Failed to load %s: %s", phosh_ticket_get_display_name (ticket), err->message); + return; + } + model = ev_document_model_new_with_document (doc); + ev_view_set_model (self->view, model); + + gtk_stack_set_visible_child_name (self->stack_tickets, "ticket-view"); + + gtk_list_box_select_row (box, NULL); +} + + +static void +on_view_close_clicked (PhoshTicketBox *self) +{ + gtk_stack_set_visible_child_name (self->stack_tickets, "tickets"); +} + + +static void +phosh_ticket_box_finalize (GObject *object) +{ + PhoshTicketBox *self = PHOSH_TICKET_BOX (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->model); + + g_clear_object (&self->monitor); + g_clear_object (&self->dir); + g_clear_pointer (&self->ticket_box_path, g_free); + + G_OBJECT_CLASS (phosh_ticket_box_parent_class)->finalize (object); +} + + +static void +phosh_ticket_box_class_init (PhoshTicketBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_ticket_box_finalize; + + g_type_ensure (EV_TYPE_VIEW); + g_type_ensure (PHOSH_TYPE_TICKET_ROW); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/ticket-box/ticket-box.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshTicketBox, lb_tickets); + gtk_widget_class_bind_template_child (widget_class, PhoshTicketBox, stack_tickets); + gtk_widget_class_bind_template_child (widget_class, PhoshTicketBox, view); + gtk_widget_class_bind_template_callback (widget_class, on_view_close_clicked); + + gtk_widget_class_set_css_name (widget_class, "phosh-ticket-box"); +} + + +static GtkWidget * +create_ticket_row (gpointer item, gpointer user_data) +{ + PhoshTicket *ticket = PHOSH_TICKET (item); + GtkWidget *row = phosh_ticket_row_new (ticket); + + return row; +} + + +static gint +ticket_compare (gconstpointer a, gconstpointer b, gpointer user_data) +{ + g_autoptr (GDateTime) dt_a = phosh_ticket_get_mod_time (PHOSH_TICKET ((gpointer)a)); + g_autoptr (GDateTime) dt_b = phosh_ticket_get_mod_time (PHOSH_TICKET ((gpointer)b)); + + return -1 * g_date_time_compare (dt_a, dt_b); +} + + +static void +on_file_child_enumerated (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GError) err = NULL; + GFile *dir = G_FILE (source_object); + GFileEnumerator *enumerator; + PhoshTicketBox *self; + const char *stack_child = "tickets"; + + enumerator = g_file_enumerate_children_finish (dir, res, &err); + if (enumerator == NULL) { + g_warning ("Failed to list %s", g_file_get_basename (dir)); + return; + } + + self = PHOSH_TICKET_BOX (user_data); + + while (TRUE) { + GFile *file; + GFileInfo *info; + g_autoptr (PhoshTicket) ticket = NULL; + + if (!g_file_enumerator_iterate (enumerator, &info, &file, self->cancel, &err)) { + g_warning ("Failed to list contents of ticket dir %s: $%s", self->ticket_box_path, err->message); + return; + } + + if (!file) + break; + + if (g_strcmp0 (g_file_info_get_content_type (info), "application/pdf") != 0) + continue; + + ticket = phosh_ticket_new (file, info); + g_list_store_insert_sorted (self->model, ticket, ticket_compare, NULL); + } + + if (g_list_model_get_n_items (G_LIST_MODEL (self->model)) == 0) + stack_child = "no-tickets"; + + gtk_stack_set_visible_child_name (self->stack_tickets, stack_child); +} + + +static void +load_tickets (PhoshTicketBox *self) +{ + g_autoptr (GSettings) settings = g_settings_new (TICKET_BOX_SCHEMA_ID); + g_autofree char *folder = NULL; + + folder = g_settings_get_string (settings, TICKET_BOX_FOLDER_KEY); + if (folder[0] != '/') + self->ticket_box_path = g_build_filename (g_get_home_dir (), folder, NULL); + else + self->ticket_box_path = g_steal_pointer (&folder); + + self->dir = g_file_new_for_path (self->ticket_box_path); + + /* + * Since the lockscreen is rebuilt on lock we don't worry about changes in directory contesnt, + * should we do, we can add a GFileMonitor later on + */ + g_file_enumerate_children_async (self->dir, + G_FILE_ATTRIBUTE_STANDARD_NAME "," + G_FILE_ATTRIBUTE_STANDARD_SYMBOLIC_ICON "," + G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME "," + G_FILE_ATTRIBUTE_TIME_MODIFIED "," + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_LOW, + self->cancel, + on_file_child_enumerated, + self); +} + + +static void +phosh_ticket_box_init (PhoshTicketBox *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + + ev_init (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->model = g_list_store_new (PHOSH_TYPE_TICKET); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/ticket-box/stylesheet/common.css"); + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self)), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_list_box_bind_model (self->lb_tickets, + G_LIST_MODEL (self->model), + create_ticket_row, + NULL, + NULL); + + g_signal_connect_swapped (self->lb_tickets, + "row-selected", + G_CALLBACK (on_row_selected), + self); + + load_tickets (self); +} diff --git a/plugins/ticket-box/ticket-box.desktop.in.in b/plugins/ticket-box/ticket-box.desktop.in.in new file mode 100644 index 000000000..237090811 --- /dev/null +++ b/plugins/ticket-box/ticket-box.desktop.in.in @@ -0,0 +1,10 @@ +[Plugin] +Id=@name@ +Name=Ticket Box +Types=lockscreen; +Comment=Show PDFs on the lock screen. This plugin is experimental. +Plugin=@plugins_dir@/libphosh-plugin-@name@.so + +[Prefs] +Id=@name@-prefs +Plugin=@plugin_prefs_dir@/libphosh-plugin-prefs-@name@.so diff --git a/plugins/ticket-box/ticket-box.h b/plugins/ticket-box/ticket-box.h new file mode 100644 index 000000000..55a69f86b --- /dev/null +++ b/plugins/ticket-box/ticket-box.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TICKET_BOX (phosh_ticket_box_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshTicketBox, phosh_ticket_box, PHOSH, TICKET_BOX, GtkBox) + +G_END_DECLS diff --git a/plugins/ticket-box/ticket-box.ui b/plugins/ticket-box/ticket-box.ui new file mode 100644 index 000000000..6eae5d6dc --- /dev/null +++ b/plugins/ticket-box/ticket-box.ui @@ -0,0 +1,101 @@ + + + + + diff --git a/plugins/ticket-box/ticket-row.c b/plugins/ticket-box/ticket-row.c new file mode 100644 index 000000000..8701f70e0 --- /dev/null +++ b/plugins/ticket-box/ticket-row.c @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "ticket-row.h" + +#include +#include + +enum { + PROP_0, + PROP_TICKET, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshTicketRow { + HdyActionRow parent; + + PhoshTicket *ticket; +}; +G_DEFINE_TYPE (PhoshTicketRow, phosh_ticket_row, HDY_TYPE_ACTION_ROW) + + +static void +phosh_ticket_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshTicketRow *self = PHOSH_TICKET_ROW (object); + + switch (property_id) { + case PROP_TICKET: + self->ticket = g_value_dup_object (value); + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (self), + phosh_ticket_get_display_name (self->ticket)); +/* TODO: by document type */ + hdy_action_row_set_icon_name (HDY_ACTION_ROW (self), "x-office-document-symbolic"); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_ticket_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTicketRow *self = PHOSH_TICKET_ROW (object); + + switch (property_id) { + case PROP_TICKET: + g_value_set_object (value, self->ticket); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_ticket_row_finalize (GObject *object) +{ + PhoshTicketRow *self = PHOSH_TICKET_ROW (object); + + g_clear_object (&self->ticket); + + G_OBJECT_CLASS (phosh_ticket_row_parent_class)->finalize (object); +} + + +static void +phosh_ticket_row_class_init (PhoshTicketRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_ticket_row_get_property; + object_class->set_property = phosh_ticket_row_set_property; + object_class->finalize = phosh_ticket_row_finalize; + + props[PROP_TICKET] = + g_param_spec_object ("ticket", "", "", + PHOSH_TYPE_TICKET, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "phosh-ticket-row"); +} + + +static void +phosh_ticket_row_init (PhoshTicketRow *self) +{ +} + + +GtkWidget * +phosh_ticket_row_new (PhoshTicket *ticket) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_TICKET_ROW, "ticket", ticket, NULL)); +} diff --git a/plugins/ticket-box/ticket-row.h b/plugins/ticket-box/ticket-row.h new file mode 100644 index 000000000..200998878 --- /dev/null +++ b/plugins/ticket-box/ticket-row.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "ticket.h" + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TICKET_ROW (phosh_ticket_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTicketRow, phosh_ticket_row, PHOSH, TICKET_ROW, HdyActionRow) + +GtkWidget *phosh_ticket_row_new (PhoshTicket *ticket); + +G_END_DECLS diff --git a/plugins/ticket-box/ticket.c b/plugins/ticket-box/ticket.c new file mode 100644 index 000000000..c700d1221 --- /dev/null +++ b/plugins/ticket-box/ticket.c @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "ticket.h" + +enum { + PROP_0, + PROP_FILE, + PROP_INFO, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshTicket { + GObject parent; + + GFile *file; + GFileInfo *info; +}; +G_DEFINE_TYPE (PhoshTicket, phosh_ticket, G_TYPE_OBJECT) + + +static void +phosh_ticket_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshTicket *self = PHOSH_TICKET (object); + + switch (property_id) { + case PROP_FILE: + self->file = g_value_dup_object (value); + break; + case PROP_INFO: + self->info = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_ticket_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTicket *self = PHOSH_TICKET (object); + + switch (property_id) { + case PROP_FILE: + g_value_set_object (value, self->file); + break; + case PROP_INFO: + g_value_set_object (value, self->info); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_ticket_finalize (GObject *object) +{ + PhoshTicket *self = PHOSH_TICKET (object); + + g_clear_object (&self->file); + g_clear_object (&self->info); + + G_OBJECT_CLASS (phosh_ticket_parent_class)->finalize (object); +} + + +static void +phosh_ticket_class_init (PhoshTicketClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_ticket_get_property; + object_class->set_property = phosh_ticket_set_property; + object_class->finalize = phosh_ticket_finalize; + + props[PROP_FILE] = + g_param_spec_object ("file", "", "", + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_INFO] = + g_param_spec_object ("info", "", "", + G_TYPE_FILE_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_ticket_init (PhoshTicket *self) +{ +} + + +PhoshTicket * +phosh_ticket_new (GFile *file, GFileInfo *info) +{ + return PHOSH_TICKET (g_object_new (PHOSH_TYPE_TICKET, + "file", file, + "info", info, + NULL)); +} + + +GFile * +phosh_ticket_get_file (PhoshTicket *self) +{ + g_return_val_if_fail (PHOSH_IS_TICKET (self), NULL); + + return self->file; +} + + +GIcon * +phosh_ticket_get_icon (PhoshTicket *self) +{ + g_return_val_if_fail (PHOSH_IS_TICKET (self), NULL); + + return g_file_info_get_symbolic_icon (self->info); +} + + +const char * +phosh_ticket_get_display_name (PhoshTicket *self) +{ + g_return_val_if_fail (PHOSH_IS_TICKET (self), NULL); + + return g_file_info_get_display_name (self->info); +} + +GDateTime * +phosh_ticket_get_mod_time (PhoshTicket *self) +{ + g_return_val_if_fail (PHOSH_IS_TICKET (self), NULL); + + return g_file_info_get_modification_date_time (self->info); +} diff --git a/plugins/ticket-box/ticket.h b/plugins/ticket-box/ticket.h new file mode 100644 index 000000000..3b20dd743 --- /dev/null +++ b/plugins/ticket-box/ticket.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TICKET (phosh_ticket_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTicket, phosh_ticket, PHOSH, TICKET, GObject) + +PhoshTicket *phosh_ticket_new (GFile *file, GFileInfo *info); +GFile *phosh_ticket_get_file (PhoshTicket *self); +const char *phosh_ticket_get_display_name (PhoshTicket *self); +GIcon *phosh_ticket_get_icon (PhoshTicket *self); +GDateTime *phosh_ticket_get_mod_time (PhoshTicket *self); + +G_END_DECLS diff --git a/plugins/upcoming-events/calendar-event.c b/plugins/upcoming-events/calendar-event.c new file mode 100644 index 000000000..b1e39f30f --- /dev/null +++ b/plugins/upcoming-events/calendar-event.c @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "calendar-event.h" + +enum { + PROP_0, + PROP_ID, + PROP_SUMMARY, + PROP_BEGIN, + PROP_END, + PROP_COLOR, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +/** + * PhoshCalendarEvent: + * + * The data for an event in a calendar + */ +struct _PhoshCalendarEvent { + GObject parent; + + char *id; + char *summary; + GDateTime *begin; + GDateTime *end; + char *color; +}; +G_DEFINE_TYPE (PhoshCalendarEvent, phosh_calendar_event, G_TYPE_OBJECT) + + +static void +phosh_calendar_event_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCalendarEvent *self = PHOSH_CALENDAR_EVENT (object); + + switch (property_id) { + case PROP_ID: + /* construct only */ + self->id = g_value_dup_string (value); + break; + case PROP_SUMMARY: + g_free (self->summary); + self->summary = g_value_dup_string (value); + break; + case PROP_BEGIN: + g_clear_pointer (&self->begin, g_date_time_unref); + self->begin = g_value_dup_boxed (value); + break; + case PROP_END: + g_clear_pointer (&self->end, g_date_time_unref); + self->end = g_value_dup_boxed (value); + break; + case PROP_COLOR: + g_free (self->color); + self->color = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_calendar_event_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCalendarEvent *self = PHOSH_CALENDAR_EVENT (object); + + switch (property_id) { + case PROP_ID: + g_value_set_string (value, self->id); + break; + case PROP_SUMMARY: + g_value_set_string (value, self->summary); + break; + case PROP_BEGIN: + g_value_set_boxed (value, self->begin); + break; + case PROP_END: + g_value_set_boxed (value, self->end); + break; + case PROP_COLOR: + g_value_set_string (value, self->color); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_calendar_event_finalize (GObject *object) +{ + PhoshCalendarEvent *self = PHOSH_CALENDAR_EVENT (object); + + g_clear_pointer (&self->id, g_free); + g_clear_pointer (&self->summary, g_free); + g_clear_pointer (&self->begin, g_date_time_unref); + g_clear_pointer (&self->end, g_date_time_unref); + g_clear_pointer (&self->color, g_free); + + G_OBJECT_CLASS (phosh_calendar_event_parent_class)->finalize (object); +} + + +static void +phosh_calendar_event_class_init (PhoshCalendarEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_calendar_event_get_property; + object_class->set_property = phosh_calendar_event_set_property; + object_class->finalize = phosh_calendar_event_finalize; + + props[PROP_ID] = + g_param_spec_string ("id", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_SUMMARY] = + g_param_spec_string ("summary", "", "", + NULL, + G_PARAM_READWRITE); + + props[PROP_BEGIN] = + g_param_spec_boxed ("begin", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE); + + props[PROP_END] = + g_param_spec_boxed ("end", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE); + + props[PROP_COLOR] = + g_param_spec_string ("color", "", "", + NULL, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_calendar_event_init (PhoshCalendarEvent *self) +{ +} + + +PhoshCalendarEvent * +phosh_calendar_event_new (const char *id, + const char *summary, + GDateTime *begin, + GDateTime *end, + const char *color) +{ + return PHOSH_CALENDAR_EVENT (g_object_new (PHOSH_TYPE_CALENDAR_EVENT, + "id", id, + "summary", summary, + "begin", begin, + "end", end, + "color", color, + NULL)); +} + + +const char * +phosh_calendar_event_get_id (PhoshCalendarEvent *self) +{ + g_return_val_if_fail (PHOSH_IS_CALENDAR_EVENT (self), NULL); + + return self->id; +} + +const char * +phosh_calendar_event_get_summary (PhoshCalendarEvent *self) +{ + g_return_val_if_fail (PHOSH_IS_CALENDAR_EVENT (self), NULL); + + return self->summary; +} + +GDateTime * +phosh_calendar_event_get_begin (PhoshCalendarEvent *self) +{ + g_return_val_if_fail (PHOSH_IS_CALENDAR_EVENT (self), NULL); + + return self->begin; +} + +GDateTime * +phosh_calendar_event_get_end (PhoshCalendarEvent *self) +{ + g_return_val_if_fail (PHOSH_IS_CALENDAR_EVENT (self), NULL); + + return self->end; +} + +const char * +phosh_calendar_event_get_color (PhoshCalendarEvent *self) +{ + g_return_val_if_fail (PHOSH_IS_CALENDAR_EVENT (self), 0); + + return self->color; +} diff --git a/plugins/upcoming-events/calendar-event.h b/plugins/upcoming-events/calendar-event.h new file mode 100644 index 000000000..8a227dade --- /dev/null +++ b/plugins/upcoming-events/calendar-event.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CALENDAR_EVENT (phosh_calendar_event_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshCalendarEvent, phosh_calendar_event, PHOSH, CALENDAR_EVENT, GObject) + +PhoshCalendarEvent *phosh_calendar_event_new (const char *id, + const char *summary, + GDateTime *begin, + GDateTime *end, + const char *color); +const char *phosh_calendar_event_get_id (PhoshCalendarEvent *self); +const char *phosh_calendar_event_get_summary (PhoshCalendarEvent *self); +GDateTime *phosh_calendar_event_get_begin (PhoshCalendarEvent *self); +GDateTime *phosh_calendar_event_get_end (PhoshCalendarEvent *self); +const char *phosh_calendar_event_get_color (PhoshCalendarEvent *self); + +G_END_DECLS diff --git a/plugins/upcoming-events/event-list.c b/plugins/upcoming-events/event-list.c new file mode 100644 index 000000000..7f8bb83fa --- /dev/null +++ b/plugins/upcoming-events/event-list.c @@ -0,0 +1,385 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "calendar-event.h" +#include "event-list.h" +#include "gtkfilterlistmodel.h" +#include "upcoming-event.h" + +#include + +enum { + PROP_0, + PROP_LABEL, + PROP_DAY_OFFSET, + PROP_TODAY, + PROP_MODEL, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +/** + * PhoshEventList: + * + * A widget that shows a list of events extracted from + * `GListModel` of `PhoshCalendarEvents` that are valid + * on `for_day`. + * + * TODO: we currently only support a single day but could + * handle any time range rather by looking at time offsets. + */ +struct _PhoshEventList { + GtkBox parent; + + GtkListBox *lb_events; + GtkLabel *label; + + GListModel *model; + GtkFilterListModel *filtered_model; + GtkStack *stack_events; + + GDateTime *today; + GDateTime *for_day; + guint day_offset; +}; +G_DEFINE_TYPE (PhoshEventList, phosh_event_list, GTK_TYPE_BOX) + + +static void +on_items_changed (PhoshEventList *self) +{ + const char *page = "no-events"; + + if (self->filtered_model && g_list_model_get_n_items (G_LIST_MODEL (self->filtered_model))) + page = "events"; + + gtk_stack_set_visible_child_name (self->stack_events, page); +} + + +static GtkWidget * +create_upcoming_event_row (gpointer item, gpointer user_data) +{ + PhoshCalendarEvent *event = PHOSH_CALENDAR_EVENT (item); + PhoshEventList *self = PHOSH_EVENT_LIST (user_data); + GtkWidget *widget; + + widget = phosh_upcoming_event_new (phosh_calendar_event_get_summary (event), + phosh_calendar_event_get_begin (event), + phosh_calendar_event_get_end (event), + self->for_day, + phosh_calendar_event_get_color (event), + /* TODO: org.gnome.desktop.interface clock-format */ + TRUE); + g_object_bind_property (event, "summary", widget, "summary", G_BINDING_DEFAULT); + g_object_bind_property (event, "begin", widget, "begin", G_BINDING_DEFAULT); + g_object_bind_property (event, "end", widget, "end", G_BINDING_DEFAULT); + g_object_bind_property (event, "color", widget, "color", G_BINDING_DEFAULT); + + return widget; +} + + +static gboolean +filter_day (gpointer item, gpointer data) +{ + int begin_day_diff, end_day_diff; + PhoshCalendarEvent *event = PHOSH_CALENDAR_EVENT (item); + PhoshEventList *self = PHOSH_EVENT_LIST (data); + GDateTime *begin = phosh_calendar_event_get_begin (event); + GDateTime *end = phosh_calendar_event_get_end (event); + g_autoptr (GDate) today = NULL; + g_autoptr (GDate) begin_date = NULL; + g_autoptr (GDate) end_date = NULL; + + today = g_date_new_dmy (g_date_time_get_day_of_month (self->today), + g_date_time_get_month (self->today), + g_date_time_get_year (self->today)); + begin_date = g_date_new_dmy (g_date_time_get_day_of_month (begin), + g_date_time_get_month (begin), + g_date_time_get_year (begin)); + end_date = g_date_new_dmy (g_date_time_get_day_of_month (end), + g_date_time_get_month (end), + g_date_time_get_year (end)); + + begin_day_diff = g_date_days_between (today, begin_date); + end_day_diff = g_date_days_between (today, end_date); + + /* Include events that start at the event-lists day */ + if (begin_day_diff == self->day_offset) + return TRUE; + + /* Include events that started before the event-lists day but are + ongoing at the lists day … */ + if (begin_day_diff < self->day_offset && end_day_diff >= self->day_offset) { + /* …but prevent multi day events to leak to the next day as they end at midnight */ + if (end_day_diff == self->day_offset && + g_date_time_get_hour (end) == 0 && + g_date_time_get_minute (end) == 0) { + return FALSE; + } else { + return TRUE; + } + } + + return FALSE; +} + + +static char * +get_label (PhoshEventList *self) +{ + switch (self->day_offset) { + case 0: + return g_strdup (_("Today")); + case 1: + return g_strdup (_("Tomorrow")); + case 2 ... 7: { + /* Translators: An event/appointment is happening on that day of the week (e.g. Tuesday) */ + return g_date_time_format (self->for_day, "%A"); + } + default: + /* Translators: The current date and weekday for display in a calendar entry */ + return g_date_time_format (self->for_day, _("%x %a")); + } +} + + +static void +phosh_event_list_set_day_offset (PhoshEventList *self, guint offset) +{ + g_autofree char *str = NULL; + + self->day_offset = offset; + + g_clear_pointer (&self->for_day, g_date_time_unref); + self->for_day = g_date_time_add_days (self->today, self->day_offset); + + str = get_label (self); + gtk_label_set_label (self->label, str); + + if (self->filtered_model) + gtk_filter_list_model_refilter (self->filtered_model); +} + + +static void +phosh_event_list_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEventList *self = PHOSH_EVENT_LIST (object); + + switch (property_id) { + case PROP_DAY_OFFSET: + phosh_event_list_set_day_offset (self, g_value_get_uint (value)); + break; + case PROP_TODAY: + phosh_event_list_set_today (self, g_value_get_boxed (value)); + break; + case PROP_MODEL: + phosh_event_list_bind_model (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_event_list_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEventList *self = PHOSH_EVENT_LIST (object); + + switch (property_id) { + case PROP_LABEL: + g_value_set_string (value, gtk_label_get_label (self->label)); + break; + case PROP_DAY_OFFSET: + g_value_set_uint (value, self->day_offset); + break; + case PROP_TODAY: + g_value_set_boxed (value, self->model); + break; + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_event_list_dispose (GObject *object) +{ + PhoshEventList *self = PHOSH_EVENT_LIST (object); + + phosh_event_list_bind_model (self, NULL); + + G_OBJECT_CLASS (phosh_event_list_parent_class)->dispose (object); +} + + +static void +phosh_event_list_finalize (GObject *object) +{ + PhoshEventList *self = PHOSH_EVENT_LIST (object); + + g_clear_pointer (&self->today, g_date_time_unref); + g_clear_pointer (&self->for_day, g_date_time_unref); + + G_OBJECT_CLASS (phosh_event_list_parent_class)->finalize (object); +} + + +static void +phosh_event_list_class_init (PhoshEventListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_event_list_get_property; + object_class->set_property = phosh_event_list_set_property; + object_class->dispose = phosh_event_list_dispose; + object_class->finalize = phosh_event_list_finalize; + + /** + * PhoshEventList:label: + * + * The label (e.g. "tomorrow") of this event list. + */ + props[PROP_LABEL] = + g_param_spec_string ("label", "", "", + NULL, + G_PARAM_READABLE); + /** + * PhoshEventList:day-offset: + * + * The offset in days from the reference date. Events from the reference date + * till the offset days will be shown in the list. + */ + props[PROP_DAY_OFFSET] = + g_param_spec_uint ("day-offset", "", "", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + /** + * PhoshEventList:today: + * + * The reference date used as current base. + */ + props[PROP_TODAY] = + g_param_spec_boxed ("today", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshEventList:model: + * + * + * The model to extract the calendar events from. + */ + props[PROP_MODEL] = + g_param_spec_object ("model", "", "", + G_TYPE_LIST_MODEL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/upcoming-events/event-list.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshEventList, label); + gtk_widget_class_bind_template_child (widget_class, PhoshEventList, lb_events); + gtk_widget_class_bind_template_child (widget_class, PhoshEventList, stack_events); +} + + +static void +phosh_event_list_init (PhoshEventList *self) +{ + self->today = g_date_time_new_now_local (); + /* Not initialized */ + self->day_offset = G_MAXUINT; + + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +void +phosh_event_list_bind_model (PhoshEventList *self, GListModel *model) +{ + g_return_if_fail (PHOSH_IS_EVENT_LIST (self)); + g_return_if_fail (G_IS_LIST_MODEL (model) || model == NULL); + g_return_if_fail (self->today != NULL); + g_return_if_fail (self->day_offset != G_MAXUINT); + + if (self->model == model) + return; + + g_set_object (&self->model, model); + if (self->filtered_model) + g_signal_handlers_disconnect_by_data (self->filtered_model, self); + g_clear_object (&self->filtered_model); + + if (self->model) { + self->filtered_model = gtk_filter_list_model_new (self->model, + filter_day, + self, + NULL); + gtk_list_box_bind_model (self->lb_events, + G_LIST_MODEL (self->filtered_model), + create_upcoming_event_row, + self, NULL); + + g_signal_connect_swapped (self->filtered_model, + "items-changed", + G_CALLBACK (on_items_changed), + self); + } else { + gtk_list_box_bind_model (self->lb_events, + NULL, NULL, NULL, NULL); + } + on_items_changed (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODEL]); +} + + +void +phosh_event_list_set_today (PhoshEventList *self, GDateTime *today) +{ + g_return_if_fail (PHOSH_IS_EVENT_LIST (self)); + + g_clear_pointer (&self->today, g_date_time_unref); + + if (today == NULL) + return; + + self->today = g_date_time_ref (today); + /* Refresh label and events */ + phosh_event_list_set_day_offset (self, self->day_offset); +} + + +uint +phosh_event_list_get_n_events (PhoshEventList *self) +{ + g_return_val_if_fail (PHOSH_IS_EVENT_LIST (self), 0); + + return g_list_model_get_n_items (G_LIST_MODEL (self->filtered_model)); +} diff --git a/plugins/upcoming-events/event-list.h b/plugins/upcoming-events/event-list.h new file mode 100644 index 000000000..e41e9de4d --- /dev/null +++ b/plugins/upcoming-events/event-list.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EVENT_LIST (phosh_event_list_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEventList, phosh_event_list, PHOSH, EVENT_LIST, GtkBox) + +void phosh_event_list_bind_model (PhoshEventList *self, GListModel *model); +void phosh_event_list_set_today (PhoshEventList *self, GDateTime *today); +uint phosh_event_list_get_n_events (PhoshEventList *self); + +G_END_DECLS diff --git a/plugins/upcoming-events/event-list.ui b/plugins/upcoming-events/event-list.ui new file mode 100644 index 000000000..744ec1a1a --- /dev/null +++ b/plugins/upcoming-events/event-list.ui @@ -0,0 +1,52 @@ + + + + + diff --git a/plugins/upcoming-events/icons/upcoming-events-all-symbolic.svg b/plugins/upcoming-events/icons/upcoming-events-all-symbolic.svg new file mode 100644 index 000000000..df27b4613 --- /dev/null +++ b/plugins/upcoming-events/icons/upcoming-events-all-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/upcoming-events/icons/upcoming-events-skip-empty-symbolic.svg b/plugins/upcoming-events/icons/upcoming-events-skip-empty-symbolic.svg new file mode 100644 index 000000000..5617a3928 --- /dev/null +++ b/plugins/upcoming-events/icons/upcoming-events-skip-empty-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/upcoming-events/meson.build b/plugins/upcoming-events/meson.build new file mode 100644 index 000000000..c7c64e543 --- /dev/null +++ b/plugins/upcoming-events/meson.build @@ -0,0 +1,74 @@ +name = 'upcoming-events' + +upcoming_events_plugin_deps = [gmobile_dep, plugin_dep] + +upcoming_events_resources = gnome.compile_resources( + 'phosh-plugin-upcoming-events-resources', + 'phosh-plugin-upcoming-events.gresources.xml', + c_name: 'phosh_plugin_upcoming_events', +) + +upcoming_events_generated_dbus_sources = gnome.gdbus_codegen( + 'phosh-plugin-upcoming-events-phosh-calendar-dbus', + app_id + '.CalendarServer.xml', + interface_prefix: app_id, + namespace: 'PhoshPluginDBus', +) + +upcoming_events_plugin_sources = files( + 'calendar-event.c', + 'calendar-event.h', + 'event-list.c', + 'event-list.h', + 'phosh-plugin-upcoming-events.c', + 'upcoming-event.c', + 'upcoming-event.h', + 'upcoming-events.c', + 'upcoming-events.h', +) + +phosh_upcoming_events_plugin = shared_module( + 'phosh-plugin-upcoming-events', + upcoming_events_plugin_sources, + upcoming_events_resources, + upcoming_events_generated_dbus_sources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), + '-DPLUGIN_NAME="@0@"'.format(name), + ], + dependencies: upcoming_events_plugin_deps, + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) +pluginconf.set('plugin_prefs_dir', plugin_prefs_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +upcoming_events_schema = 'sm.puri.phosh.plugins.upcoming-events.gschema.xml' +compiled = gnome.compile_schemas(depend_files: upcoming_events_schema) +compile_schemas = find_program('glib-compile-schemas', required: false) +if compile_schemas.found() + test( + 'Validate @0@ schema file'.format(upcoming_events_schema), + compile_schemas, + args: ['--strict', '--dry-run', meson.current_source_dir()], + ) +endif +install_data(upcoming_events_schema, install_dir: 'share/glib-2.0/schemas') + +subdir('prefs') diff --git a/plugins/upcoming-events/mobi.phosh.Shell.CalendarServer.xml b/plugins/upcoming-events/mobi.phosh.Shell.CalendarServer.xml new file mode 100644 index 000000000..c100d7ca3 --- /dev/null +++ b/plugins/upcoming-events/mobi.phosh.Shell.CalendarServer.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/upcoming-events/phosh-plugin-upcoming-events.c b/plugins/upcoming-events/phosh-plugin-upcoming-events.c new file mode 100644 index 000000000..c57c34f67 --- /dev/null +++ b/plugins/upcoming-events/phosh-plugin-upcoming-events.c @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "upcoming-events.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_upcoming_events_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, + PHOSH_TYPE_UPCOMING_EVENTS, + PLUGIN_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_upcoming_events_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/upcoming-events/phosh-plugin-upcoming-events.gresources.xml b/plugins/upcoming-events/phosh-plugin-upcoming-events.gresources.xml new file mode 100644 index 000000000..c00986e96 --- /dev/null +++ b/plugins/upcoming-events/phosh-plugin-upcoming-events.gresources.xml @@ -0,0 +1,11 @@ + + + + event-list.ui + upcoming-event.ui + upcoming-events.ui + stylesheet/common.css + icons/upcoming-events-all-symbolic.svg + icons/upcoming-events-skip-empty-symbolic.svg + + diff --git a/plugins/upcoming-events/prefs/meson.build b/plugins/upcoming-events/prefs/meson.build new file mode 100644 index 000000000..453403f70 --- /dev/null +++ b/plugins/upcoming-events/prefs/meson.build @@ -0,0 +1,25 @@ +upcoming_events_prefs_resources = gnome.compile_resources( + 'phosh-plugin-prefs-upcoming-events-resources', + 'phosh-plugin-prefs-upcoming-events.gresources.xml', + c_name: 'phosh_plugin_prefs_upcoming_events', +) + +upcoming_events_plugin_prefs_sources = files( + 'phosh-plugin-prefs-upcoming-events.c', + 'upcoming-events-prefs.c', + 'upcoming-events-prefs.h', +) + +phosh_upcoming_events_plugin_prefs = shared_module( + 'phosh-plugin-prefs-upcoming-events', + upcoming_events_plugin_prefs_sources, + upcoming_events_prefs_resources, + c_args: [ + '-DG_LOG_DOMAIN="phosh-plugin-prefs-upcoming-events"', + '-DPLUGIN_PREFS_NAME="@0@-prefs"'.format(name), + '-DADW_VERSION_MIN_REQUIRED=@0@'.format(adw_ver_str), + ], + dependencies: plugin_prefs_dep, + install: true, + install_dir: plugin_prefs_dir, +) diff --git a/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.c b/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.c new file mode 100644 index 000000000..f408eaa67 --- /dev/null +++ b/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "phosh-plugin-prefs-config.h" + +#include "upcoming-events-prefs.h" + +#include "phosh-plugin.h" + +char **g_io_phosh_plugin_prefs_upcoming_events_query (void); + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, + PHOSH_TYPE_UPCOMING_EVENTS_PREFS, + PLUGIN_PREFS_NAME, + 10); +} + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_prefs_upcoming_events_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_LOCKSCREEN_WIDGET_PREFS, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.gresources.xml b/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.gresources.xml new file mode 100644 index 000000000..66deeb874 --- /dev/null +++ b/plugins/upcoming-events/prefs/phosh-plugin-prefs-upcoming-events.gresources.xml @@ -0,0 +1,6 @@ + + + + upcoming-events-prefs.ui + + diff --git a/plugins/upcoming-events/prefs/upcoming-events-prefs.c b/plugins/upcoming-events/prefs/upcoming-events-prefs.c new file mode 100644 index 000000000..2722a0ec4 --- /dev/null +++ b/plugins/upcoming-events/prefs/upcoming-events-prefs.c @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "phosh-plugin-prefs-config.h" + +#include "upcoming-events-prefs.h" + +#define UPCOMING_EVENTS_SCHEMA_ID "sm.puri.phosh.plugins.upcoming-events" +#define UPCOMING_EVENTS_DAYS_KEY "days" + +/** + * PhoshUpcomingEventsPrefs: + * + * Preferences for upcoming-events plugin. + */ +struct _PhoshUpcomingEventsPrefs { + AdwPreferencesDialog parent; + + GtkAdjustment *adjustment; + + GSettings *settings; +}; + +G_DEFINE_TYPE (PhoshUpcomingEventsPrefs, phosh_upcoming_events_prefs, ADW_TYPE_PREFERENCES_DIALOG); + + +static void +phosh_upcoming_events_prefs_finalize (GObject *object) +{ + PhoshUpcomingEventsPrefs *self = PHOSH_UPCOMING_EVENTS_PREFS (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_upcoming_events_prefs_parent_class)->finalize (object); +} + +static void +phosh_upcoming_events_prefs_class_init (PhoshUpcomingEventsPrefsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_upcoming_events_prefs_finalize; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/upcoming-events-prefs/upcoming-events-prefs.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEventsPrefs, adjustment); +} + + +static void +phosh_upcoming_events_prefs_init (PhoshUpcomingEventsPrefs *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (UPCOMING_EVENTS_SCHEMA_ID); + gtk_adjustment_set_upper (self->adjustment, G_MAXUINT); + g_settings_bind (self->settings, UPCOMING_EVENTS_DAYS_KEY, + self->adjustment, "value", + G_SETTINGS_BIND_DEFAULT); +} diff --git a/plugins/upcoming-events/prefs/upcoming-events-prefs.h b/plugins/upcoming-events/prefs/upcoming-events-prefs.h new file mode 100644 index 000000000..271a77107 --- /dev/null +++ b/plugins/upcoming-events/prefs/upcoming-events-prefs.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_UPCOMING_EVENTS_PREFS (phosh_upcoming_events_prefs_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshUpcomingEventsPrefs, phosh_upcoming_events_prefs, PHOSH, UPCOMING_EVENTS_PREFS, AdwPreferencesDialog) + +G_END_DECLS diff --git a/plugins/upcoming-events/prefs/upcoming-events-prefs.ui b/plugins/upcoming-events/prefs/upcoming-events-prefs.ui new file mode 100644 index 000000000..2dccff0e9 --- /dev/null +++ b/plugins/upcoming-events/prefs/upcoming-events-prefs.ui @@ -0,0 +1,32 @@ + + + + + + diff --git a/plugins/upcoming-events/sm.puri.phosh.plugins.upcoming-events.gschema.xml b/plugins/upcoming-events/sm.puri.phosh.plugins.upcoming-events.gschema.xml new file mode 100644 index 000000000..9f1b444ac --- /dev/null +++ b/plugins/upcoming-events/sm.puri.phosh.plugins.upcoming-events.gschema.xml @@ -0,0 +1,16 @@ + + + + + 7 + Number of days to display + The number of days for which the events should be displayed. + + + false + Whether to exclude days without any events + Controls whether days with no events should be displayed. + + + diff --git a/plugins/upcoming-events/stylesheet/common.css b/plugins/upcoming-events/stylesheet/common.css new file mode 100644 index 000000000..5e0553e05 --- /dev/null +++ b/plugins/upcoming-events/stylesheet/common.css @@ -0,0 +1,35 @@ +phosh-upcoming-events { + background-color: @phosh_notification_bg_color; + border-radius: 12px; + padding: 8px; +} + +phosh-upcoming-events list { + background: none; +} + +phosh-upcoming-events list row { + background: none; +} + +phosh-upcoming-events .event-list-listbox { + background-color: transparent; +} + +phosh-upcoming-events .event-list-listbox row { + border-radius: 6px; + background: shade(@phosh_notification_bg_color, 1.3); +} + +phosh-upcoming-events .event-list-listbox row:hover { + background: shade(@phosh_notification_bg_color, 1.5); +} + +phosh-upcoming-events .event-list-listbox row:active { + background: shade(@phosh_notification_bg_color, 1.8); +} + +phosh-upcoming-events phosh-upcoming-event .color-bar { + border-radius: 6px; + background-color: currentColor; +} diff --git a/plugins/upcoming-events/upcoming-event.c b/plugins/upcoming-events/upcoming-event.c new file mode 100644 index 000000000..7388b7f2d --- /dev/null +++ b/plugins/upcoming-events/upcoming-event.c @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "upcoming-event.h" + +#include + +enum { + PROP_0, + PROP_SUMMARY, + PROP_BEGIN, + PROP_END, + PROP_COLOR, + PROP_IS_24H, + PROP_FOR_DAY, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +#define COLOR_BAR_CSS "phosh-upcoming-events phosh-upcoming-event .color-bar {" \ + " background-color: %s; " \ + "} " + +struct _PhoshUpcomingEvent { + GtkBox parent; + + GtkLabel *lbl_begin; + GtkDrawingArea *color_bar; + GtkLabel *lbl_summary; + + GDateTime *begin; + GDateTime *end; + GDateTime *for_day; + + char *color; + GtkCssProvider *color_css; + gboolean is_24h; +}; +G_DEFINE_TYPE (PhoshUpcomingEvent, phosh_upcoming_event, GTK_TYPE_BOX) + + +static void +phosh_upcoming_event_format_time (PhoshUpcomingEvent *self, GString *string, GDateTime *datetime) +{ + g_autofree char *str = NULL; + + if (self->is_24h) { + /* Translators: This is the time format used in 24-hour mode. */ + str = g_date_time_format (datetime, _("%R")); + } else { + /* Translators: This is the time format used in 12-hour mode. */ + str = g_date_time_format (datetime, _("%l:%M %p")); + } + + g_string_append (string, str); +} + + +static gboolean +on_same_day (GDateTime *dt1, GDateTime *dt2) +{ + return (g_date_time_get_year (dt1) == g_date_time_get_year (dt2) && + g_date_time_get_month (dt1) == g_date_time_get_month (dt2) && + g_date_time_get_day_of_month (dt1) == g_date_time_get_day_of_month (dt2)); +} + +/** + * phosh_upcoming_event_starts_day_before: + * @self: The event + * @self: The `GDateTime` to compare to + * + * Returns: %TRUE if the events starts on a day before the day of `dt`. + */ +static gboolean +phosh_upcoming_event_starts_before (PhoshUpcomingEvent *self, GDateTime *dt) +{ + return (on_same_day (self->begin, dt) == FALSE && + g_date_time_get_year (self->begin) <= g_date_time_get_year (dt) && + g_date_time_get_month (self->begin) <= g_date_time_get_month (dt) && + g_date_time_get_day_of_month (self->begin) <= g_date_time_get_day_of_month (dt)); +} + +/** + * phosh_upcoming_event_ends_day_after: + * @self: The event + * @self: The `GDateTime` to compare to + * + * Returns: %TRUE if the events ends on a day after the day of `dt`. + */ +static gboolean +phosh_upcoming_event_ends_after (PhoshUpcomingEvent *self, GDateTime *dt) +{ + return (on_same_day (self->begin, dt) == FALSE && + g_date_time_get_year (self->end) >= g_date_time_get_year (dt) && + g_date_time_get_month (self->end) >= g_date_time_get_month (dt) && + g_date_time_get_day_of_month (self->end) >= g_date_time_get_day_of_month (dt)); +} + + +static void +phosh_upcoming_event_update_timespec (PhoshUpcomingEvent *self) +{ + g_autoptr (GString) string = g_string_new (NULL); + + if (self->begin == NULL || self->end == NULL || self->for_day == NULL) + return; + + /* All day event, the day we're at doesn't matter */ + if (g_date_time_get_hour (self->begin) == 0 && + g_date_time_get_minute (self->begin) == 0 && + g_date_time_get_hour (self->end) == 0 && + g_date_time_get_minute (self->end) == 0) { + + /* Translators: An all day event */ + g_string_append (string, _("All day")); + + gtk_label_set_label (self->lbl_begin, string->str); + return; + } + + /* Event starts and ends on same day */ + if (on_same_day (self->begin, self->for_day) && + on_same_day (self->end, self->for_day)) { + phosh_upcoming_event_format_time (self, string, self->begin); + g_string_append (string, "\r"); + phosh_upcoming_event_format_time (self, string, self->end); + gtk_label_set_label (self->lbl_begin, string->str); + return; + } + + /* Event starts on for_day */ + if (on_same_day (self->begin, self->for_day)) { + phosh_upcoming_event_format_time (self, string, self->begin); + gtk_label_set_label (self->lbl_begin, string->str); + return; + } + + /* Event ends on for_day */ + if (on_same_day (self->end, self->for_day)) { + /* Translators: When the event ends: Ends\r16:00 */ + g_string_append (string, _("Ends")); + g_string_append (string, "\r"); + phosh_upcoming_event_format_time (self, string, self->end); + gtk_label_set_label (self->lbl_begin, string->str); + return; + } + + /* Event starts before for_day and ends after for_day */ + if (phosh_upcoming_event_starts_before (self, self->for_day) && + phosh_upcoming_event_ends_after (self, self->for_day)) { + /* Translators: An all day event */ + g_string_append (string, _ ("All day")); + gtk_label_set_label (self->lbl_begin, string->str); + return; + } +} + + +static void +phosh_upcoming_event_set_color (PhoshUpcomingEvent *self, const char *color) +{ + g_autofree char* css = NULL; + g_autoptr (GError) err = NULL; + g_autofree char *colorstr = NULL; + GdkRGBA rgba; + + if (g_strcmp0 (self->color, color) == 0) + return; + + g_free (self->color); + self->color = g_strdup (color); + + if (gdk_rgba_parse (&rgba, color) == FALSE) + rgba.red = rgba.green = rgba.blue = 1.0; + + colorstr = gdk_rgba_to_string (&rgba); + css = g_strdup_printf (COLOR_BAR_CSS, colorstr); + if (gtk_css_provider_load_from_data (self->color_css, css, -1, &err) == FALSE) { + g_warning ("Failed to load css: %s", err->message); + return; + } +} + + +static void +phosh_upcoming_event_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshUpcomingEvent *self = PHOSH_UPCOMING_EVENT (object); + + switch (property_id) { + case PROP_SUMMARY: + phosh_upcoming_event_set_summary (self, g_value_get_string (value)); + break; + case PROP_BEGIN: + g_clear_pointer (&self->begin, g_date_time_unref); + self->begin = g_value_dup_boxed (value); + phosh_upcoming_event_update_timespec (self); + break; + case PROP_END: + g_clear_pointer (&self->end, g_date_time_unref); + self->end = g_value_dup_boxed (value); + phosh_upcoming_event_update_timespec (self); + break; + case PROP_FOR_DAY: + g_clear_pointer (&self->for_day, g_date_time_unref); + self->for_day = g_value_dup_boxed (value); + phosh_upcoming_event_update_timespec (self); + break; + case PROP_COLOR: + phosh_upcoming_event_set_color (self, g_value_get_string (value)); + break; + case PROP_IS_24H: + self->is_24h = g_value_get_boolean (value); + phosh_upcoming_event_update_timespec (self); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_upcoming_event_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshUpcomingEvent *self = PHOSH_UPCOMING_EVENT (object); + + switch (property_id) { + case PROP_SUMMARY: + g_value_set_string (value, gtk_label_get_label (self->lbl_summary)); + break; + case PROP_BEGIN: + g_value_set_boxed (value, self->begin); + break; + case PROP_END: + g_value_set_boxed (value, self->end); + break; + case PROP_FOR_DAY: + g_value_set_boxed (value, self->for_day); + break; + case PROP_COLOR: + g_value_set_string (value, self->color); + break; + case PROP_IS_24H: + g_value_set_boolean (value, self->is_24h); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_upcoming_event_finalize (GObject *object) +{ + PhoshUpcomingEvent *self = PHOSH_UPCOMING_EVENT (object); + + g_clear_pointer (&self->begin, g_date_time_unref); + g_clear_pointer (&self->end, g_date_time_unref); + g_clear_pointer (&self->for_day, g_date_time_unref); + g_clear_pointer (&self->color, g_free); + g_clear_object (&self->color_css); + + G_OBJECT_CLASS (phosh_upcoming_event_parent_class)->finalize (object); +} + + +static void +phosh_upcoming_event_class_init (PhoshUpcomingEventClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_upcoming_event_get_property; + object_class->set_property = phosh_upcoming_event_set_property; + object_class->finalize = phosh_upcoming_event_finalize; + + /** + * PhoshUpcomingEvent:summary: + * + * A summary what the event is about + */ + props[PROP_SUMMARY] = + g_param_spec_string ("summary", "", "", + NULL, + G_PARAM_READWRITE); + /** + * PhoshUpcomingEvent:start: + * + * The date and time the event starts at. + */ + props[PROP_BEGIN] = + g_param_spec_boxed ("begin", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE); + /** + * PhoshUpcomingEvent:end: + * + * The date and time the event ends at. + */ + props[PROP_END] = + g_param_spec_boxed ("end", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE); + + /** + * PhoshUpcomingEvent:for-day: + * + * The day this entry is rendered for. + */ + props[PROP_FOR_DAY] = + g_param_spec_boxed ("for-day", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE); + /** + * PhoshUpcomingEvent:for-day: + * + * The events color as parsed by `gdk_rgba_parse()`. + */ + props[PROP_COLOR] = + g_param_spec_string ("color", "", "", + NULL, + G_PARAM_READWRITE); + /** + * PhoshUpcomingEvent:is-24h + * + * Whether to use 24h clock format + */ + props[PROP_IS_24H] = + g_param_spec_boolean ("is-24h", "", "", + FALSE, + G_PARAM_READWRITE); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/upcoming-events/upcoming-event.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvent, color_bar); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvent, lbl_begin); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvent, lbl_summary); + + gtk_widget_class_set_css_name (widget_class, "phosh-upcoming-event"); +} + + +static void +phosh_upcoming_event_init (PhoshUpcomingEvent *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->color_css = gtk_css_provider_new (); + gtk_style_context_add_provider (gtk_widget_get_style_context (GTK_WIDGET (self->color_bar)), + GTK_STYLE_PROVIDER (self->color_css), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1); + self->is_24h = TRUE; +} + + +GtkWidget * +phosh_upcoming_event_new (const char *summary, + GDateTime *begin, + GDateTime *end, + GDateTime *for_day, + const char *color, + gboolean is_24h) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_UPCOMING_EVENT, + "summary", summary, + "begin", begin, + "end", end, + "color", color, + "for-day", for_day, + "is-24h", is_24h, + NULL)); +} + + +void +phosh_upcoming_event_set_summary (PhoshUpcomingEvent *self, const char *summary) +{ + g_return_if_fail (PHOSH_IS_UPCOMING_EVENT (self)); + + if (summary && summary[0] != '\0') + gtk_label_set_label (self->lbl_summary, summary); + else + gtk_label_set_label (self->lbl_summary, _("Untitled event")); +} diff --git a/plugins/upcoming-events/upcoming-event.h b/plugins/upcoming-events/upcoming-event.h new file mode 100644 index 000000000..ef8a4e3b7 --- /dev/null +++ b/plugins/upcoming-events/upcoming-event.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_UPCOMING_EVENT (phosh_upcoming_event_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshUpcomingEvent, phosh_upcoming_event, PHOSH, UPCOMING_EVENT, GtkBox) + +GtkWidget *phosh_upcoming_event_new (const char *summary, + GDateTime *when, + GDateTime *end, + GDateTime *for_day, + const char *color, + gboolean is_24h); +void phosh_upcoming_event_set_summary (PhoshUpcomingEvent *self, + const char *summary); + +G_END_DECLS diff --git a/plugins/upcoming-events/upcoming-event.ui b/plugins/upcoming-events/upcoming-event.ui new file mode 100644 index 000000000..fbdd4aeca --- /dev/null +++ b/plugins/upcoming-events/upcoming-event.ui @@ -0,0 +1,43 @@ + + + + + diff --git a/plugins/upcoming-events/upcoming-events.c b/plugins/upcoming-events/upcoming-events.c new file mode 100644 index 000000000..959e838f1 --- /dev/null +++ b/plugins/upcoming-events/upcoming-events.c @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#include "phosh-config.h" + +#include "event-list.h" +#include "calendar-event.h" +#include "upcoming-events.h" +#include "gtkfilterlistmodel.h" + +#include "phosh-plugin-upcoming-events-phosh-calendar-dbus.h" + +#include +#include +#include + +#define UPCOMING_EVENTS_SCHEMA_ID "sm.puri.phosh.plugins.upcoming-events" +#define UPCOMING_EVENT_DAYS_KEY "days" +#define UPCOMING_EVENT_SKIP_DAYS_KEY "skip-empty" + +#define EXPAND_LIST_ICON "upcoming-events-all-symbolic" +#define SHRINK_LIST_ICON "upcoming-events-skip-empty-symbolic" + +/** + * PhoshUpcomgingEvents: + * + * Shows the next upcoming calendar events + */ +struct _PhoshUpcomingEvents { + GtkBox parent; + + PhoshPluginDBusCalendarServer *proxy; + GCancellable *cancel; + + GtkStack *stack; + GtkListBox *events_box; + GListModel *event_lists; + GtkFilterListModel *event_lists_filtered; + GListStore *events; + GHashTable *event_ids; + GDateTime *since; + guint num_days; + gboolean skip_empty; + GtkToggleButton *skip_empty_btn; + + GSettings *settings; + GFileMonitor *tz_monitor; + guint today_changed_timeout_id; +}; + +G_DEFINE_TYPE (PhoshUpcomingEvents, phosh_upcoming_events, GTK_TYPE_BOX); + + +static void +phosh_upcoming_events_finalize (GObject *object) +{ + PhoshUpcomingEvents *self = PHOSH_UPCOMING_EVENTS (object); + + g_clear_object (&self->event_lists_filtered); + g_clear_handle_id (&self->today_changed_timeout_id, g_source_remove); + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->events); + g_clear_object (&self->settings); + g_clear_object (&self->tz_monitor); + g_clear_pointer (&self->event_ids, g_hash_table_unref); + g_clear_pointer (&self->since, g_date_time_unref); + + G_OBJECT_CLASS (phosh_upcoming_events_parent_class)->finalize (object); +} + + +static void +on_skip_empty_btn_toggled (GtkToggleButton *btn, + PhoshUpcomingEvents *self) +{ + self->skip_empty = !self->skip_empty; + + g_settings_set_boolean (self->settings, "skip-empty", self->skip_empty); +} + + +static void +phosh_upcoming_events_class_init (PhoshUpcomingEventsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_upcoming_events_finalize; + + g_type_ensure (PHOSH_TYPE_EVENT_LIST); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/upcoming-events/upcoming-events.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvents, stack); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvents, events_box); + gtk_widget_class_bind_template_child (widget_class, PhoshUpcomingEvents, skip_empty_btn); + + gtk_widget_class_bind_template_callback (widget_class, on_skip_empty_btn_toggled); + + gtk_widget_class_set_css_name (widget_class, "phosh-upcoming-events"); +} + + +static void +on_set_time_range_finish (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr (GError) err = NULL; + PhoshPluginDBusCalendarServer *proxy = PHOSH_PLUGIN_DBUS_CALENDAR_SERVER (source_object); + gboolean success; + + success = phosh_plugin_dbus_calendar_server_call_set_time_range_finish (proxy, res, &err); + if (!success) { + g_warning ("Failed to set time range: %s", err->message); + return; + } +} + + +static void +load_events (PhoshUpcomingEvents *self, gboolean force_reload) +{ + g_autoptr (GDateTime) until = NULL; + g_autofree char *since_str = g_date_time_format_iso8601 (self->since); + g_autofree char *until_str = NULL; + + until = g_date_time_add_days (self->since, self->num_days); + until_str = g_date_time_format_iso8601 (until); + + g_debug ("Requesting events from %s to %s", since_str, until_str); + + phosh_plugin_dbus_calendar_server_call_set_time_range (self->proxy, + g_date_time_to_unix (self->since), + g_date_time_to_unix (until), + force_reload, + self->cancel, + on_set_time_range_finish, + self); +} + + +static gint +calendar_event_begin_compare (gconstpointer a, + gconstpointer b, + gpointer user_data) +{ + PhoshCalendarEvent *ca = PHOSH_CALENDAR_EVENT ((gpointer)a); + PhoshCalendarEvent *cb = PHOSH_CALENDAR_EVENT ((gpointer)b); + + return g_date_time_compare (phosh_calendar_event_get_begin (ca), + phosh_calendar_event_get_begin (cb)); +} + + +static void +refilter_event_lists (PhoshUpcomingEvents *self) +{ + const char *child_name = "no-events"; + + gtk_filter_list_model_refilter (self->event_lists_filtered); + + if (!self->skip_empty || g_list_model_get_n_items (G_LIST_MODEL (self->event_lists_filtered))) + child_name = "events-window"; + + gtk_stack_set_visible_child_name (self->stack, child_name); +} + + +#define EVENT_FORMAT "(&s&sxx@a{sv})" + +static void +on_events_added_or_updated (PhoshUpcomingEvents *self, GVariant *events) +{ + GVariantIter iter; + gint64 begin, end; + const char *id, *summary; + GVariant *extra_dict; + gboolean changed = FALSE; + + g_variant_iter_init (&iter, events); + while (g_variant_iter_next (&iter, EVENT_FORMAT, &id, &summary, &begin, &end, &extra_dict)) { + PhoshCalendarEvent *event; + g_auto (GVariantDict) dict = G_VARIANT_DICT_INIT (extra_dict); + const char *color; + + if (g_variant_dict_lookup (&dict, "color", "&s", &color) == FALSE) + color = "#ffffff"; + + event = g_hash_table_lookup (self->event_ids, id); + if (event) { + g_object_set (event, + "summary", summary, + "begin", g_date_time_new_from_unix_local (begin), + "end", g_date_time_new_from_unix_local (end), + "color", color, + NULL); + changed = TRUE; + continue; + } + + event = phosh_calendar_event_new (id, + summary, + g_date_time_new_from_unix_local (begin), + g_date_time_new_from_unix_local (end), + color); + g_hash_table_insert (self->event_ids, g_strdup (id), g_object_ref (event)); + g_list_store_insert_sorted (self->events, + g_steal_pointer (&event), + calendar_event_begin_compare, + NULL); + } + + refilter_event_lists (self); + + if (changed == FALSE) + return; + + /* Changed events might be tz change so refilter days */ + for (int i = 0; i < g_list_model_get_n_items (self->event_lists); i++) { + g_autoptr (PhoshEventList) el = g_list_model_get_item (self->event_lists, i); + + phosh_event_list_set_today (el, self->since); + } +} + +#undef EVENT_FORMAT + +static void +on_events_removed (PhoshUpcomingEvents *self, GStrv ids) +{ + int removed = 0; + + for (int i = 0; i < g_strv_length (ids); i++) { + const char *id = ids[i]; + PhoshCalendarEvent *event; + guint pos; + + event = g_hash_table_lookup (self->event_ids, id); + if (!event) + continue; + + if (g_list_store_find (self->events, event, &pos)) { + g_list_store_remove (self->events, pos); + removed++; + } else { + g_warning ("Found %s in hash but not in list", id); + } + + g_hash_table_remove (self->event_ids, id); + } + + refilter_event_lists (self); + + g_debug ("Removed %d events of %d", removed, g_strv_length (ids)); +} + + +static void setup_date_change_timeout (PhoshUpcomingEvents *self); + + +static void +update_calendar (PhoshUpcomingEvents *self, gboolean force_reload) +{ + g_clear_pointer (&self->since, g_date_time_unref); + self->since = g_date_time_new_now_local (); + + load_events (self, force_reload); + + for (int i = 0; i < g_list_model_get_n_items (self->event_lists); i++) { + g_autoptr (PhoshEventList) el = g_list_model_get_item (self->event_lists, i); + + phosh_event_list_set_today (el, self->since); + } + + /* Rearm timer */ + setup_date_change_timeout (self); +} + + +static void +on_client_disappeared (PhoshUpcomingEvents *self, const char *client_id) +{ + g_debug ("Client %s gone", client_id); + + /* Update the whole calendar */ + g_list_store_remove_all (self->events); + g_hash_table_remove_all (self->event_ids); + update_calendar (self, TRUE); +} + + +static void +on_today_changed (gpointer data) +{ + PhoshUpcomingEvents *self = PHOSH_UPCOMING_EVENTS (data); + + g_debug ("Date change, reloading events"); + + update_calendar (self, FALSE); +} + + +static void +on_tz_changed (PhoshUpcomingEvents *self, + GFile *file, + GFile *other_file, + GFileMonitorEvent *event, + GFileMonitor *monitor) +{ + g_debug ("Timezone changed"); + + update_calendar (self, TRUE); +} + + +static void +setup_date_change_timeout (PhoshUpcomingEvents *self) +{ + g_autoptr (GDateTime) now = NULL; + g_autoptr (GDateTime) nextday = NULL; + g_autoptr (GDateTime) tomorrow = NULL; + guint seconds; + GTimeSpan span; + + now = g_date_time_new_now_local (); + nextday = g_date_time_add_days (now, 1); + tomorrow = g_date_time_new (g_date_time_get_timezone (nextday), + g_date_time_get_year (nextday), + g_date_time_get_month (nextday), + g_date_time_get_day_of_month (nextday), + 0, + 0, + 0.0); + span = g_date_time_difference (tomorrow, now); + + seconds = 1 + (span / G_TIME_SPAN_SECOND); + + g_debug ("Arming day change timer for %d seconds", seconds); + self->today_changed_timeout_id = gm_timeout_add_seconds_once (seconds, + on_today_changed, + self); +} + + +static void +on_skip_empty_changed (PhoshUpcomingEvents *self) +{ + const char *icon_name; + + self->skip_empty = g_settings_get_boolean (self->settings, UPCOMING_EVENT_SKIP_DAYS_KEY); + icon_name = self->skip_empty ? EXPAND_LIST_ICON : SHRINK_LIST_ICON; + + gtk_button_set_image (GTK_BUTTON (self->skip_empty_btn), + gtk_image_new_from_icon_name (icon_name, GTK_ICON_SIZE_BUTTON)); + + refilter_event_lists (self); +} + + +static gboolean +filter_event_lists_func (gpointer item, gpointer user_data) +{ + PhoshUpcomingEvents *self = PHOSH_UPCOMING_EVENTS (user_data); + PhoshEventList *event_list = PHOSH_EVENT_LIST (item); + + if (!self->skip_empty) + return TRUE; + + return phosh_event_list_get_n_events (event_list) > 0; +} + + +static GtkWidget * +create_event_list_func (gpointer item, gpointer user_data) +{ + PhoshUpcomingEvents *self = PHOSH_UPCOMING_EVENTS (user_data); + PhoshEventList *el; + int day_offset; + + g_object_get (G_OBJECT (item), "day-offset", &day_offset, NULL); + + el = g_object_new (PHOSH_TYPE_EVENT_LIST, + "day-offset", day_offset, + "today", self->since, + "model", self->events, + "visible", TRUE, + NULL); + + return GTK_WIDGET (el); +} + + +static void +on_num_days_changed (PhoshUpcomingEvents *self) +{ + g_autofree char *desc = NULL; + HdyStatusPage *no_events = HDY_STATUS_PAGE (gtk_stack_get_child_by_name (self->stack, + "no-events")); + + self->num_days = g_settings_get_uint (self->settings, UPCOMING_EVENT_DAYS_KEY); + desc = g_strdup_printf (_("No events for the next %d days"), self->num_days); + + hdy_status_page_set_description (no_events, desc); + + g_debug ("Number of days changed to %u; reconfiguring event lists", self->num_days); + + g_list_store_remove_all (G_LIST_STORE (self->event_lists)); + + for (int i = 0; i < self->num_days; i++) { + GtkWidget *event_list = g_object_new (PHOSH_TYPE_EVENT_LIST, + "day-offset", i, + "today", self->since, + "model", self->events, + "visible", TRUE, + NULL); + + g_list_store_append (G_LIST_STORE (self->event_lists), event_list); + } + + gtk_list_box_bind_model (self->events_box, + G_LIST_MODEL (self->event_lists_filtered), + create_event_list_func, + self, + NULL); + + load_events (self, FALSE); +} + + +static void +on_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr (GError) err = NULL; + PhoshUpcomingEvents *self; + PhoshPluginDBusCalendarServer *proxy; + + proxy = phosh_plugin_dbus_calendar_server_proxy_new_for_bus_finish (res, &err); + if (proxy == NULL) { + g_warning ("Failed to get CalendarServer proxy: %s", err->message); + return; + } + self = PHOSH_UPCOMING_EVENTS (data); + self->proxy = proxy; + + g_debug ("CalendarServer initialized"); + g_object_connect (self->proxy, + "swapped-object-signal::events-added-or-updated", + G_CALLBACK (on_events_added_or_updated), self, + "swapped-object-signal::events-removed", + G_CALLBACK (on_events_removed), self, + "swapped-object-signal::client-disappeared", + G_CALLBACK (on_client_disappeared), self, + NULL); + + on_today_changed (self); + on_num_days_changed (self); +} + + +static void +phosh_upcoming_events_init (PhoshUpcomingEvents *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + g_autoptr (GFile) tz = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (UPCOMING_EVENTS_SCHEMA_ID); + g_signal_connect_object (self->settings, + "changed::days", + G_CALLBACK (on_num_days_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->settings, + "changed::skip-empty", + G_CALLBACK (on_skip_empty_changed), + self, + G_CONNECT_SWAPPED); + + self->event_lists = G_LIST_MODEL (g_list_store_new (PHOSH_TYPE_EVENT_LIST)); + self->event_lists_filtered = gtk_filter_list_model_new (self->event_lists, + filter_event_lists_func, + self, + NULL); + self->events = g_list_store_new (PHOSH_TYPE_CALENDAR_EVENT); + + self->event_ids = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + + on_skip_empty_changed (self); + + self->cancel = g_cancellable_new (); + phosh_plugin_dbus_calendar_server_proxy_new_for_bus ( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION, + PHOSH_APP_ID ".CalendarServer", + PHOSH_DBUS_PATH_PREFIX "/CalendarServer", + self->cancel, + on_proxy_new_for_bus_finish, + self); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/upcoming-events/stylesheet/common.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/upcoming-events/icons"); + + tz = g_file_new_for_path ("/etc/localtime"); + self->tz_monitor = g_file_monitor_file (tz, 0, NULL, NULL); + g_signal_connect_swapped (self->tz_monitor, "changed", G_CALLBACK (on_tz_changed), self); +} diff --git a/plugins/upcoming-events/upcoming-events.desktop.in.in b/plugins/upcoming-events/upcoming-events.desktop.in.in new file mode 100644 index 000000000..42709229e --- /dev/null +++ b/plugins/upcoming-events/upcoming-events.desktop.in.in @@ -0,0 +1,10 @@ +[Plugin] +Id=@name@ +Name=Upcoming Events +Types=lockscreen; +Comment=Show upcoming calendar events +Plugin=@plugins_dir@/libphosh-plugin-@name@.so + +[Prefs] +Id=@name@-prefs +Plugin=@plugin_prefs_dir@/libphosh-plugin-prefs-@name@.so diff --git a/plugins/upcoming-events/upcoming-events.h b/plugins/upcoming-events/upcoming-events.h new file mode 100644 index 000000000..86c5f644f --- /dev/null +++ b/plugins/upcoming-events/upcoming-events.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_UPCOMING_EVENTS (phosh_upcoming_events_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshUpcomingEvents, phosh_upcoming_events, PHOSH, UPCOMING_EVENTS, GtkBox) + +G_END_DECLS diff --git a/plugins/upcoming-events/upcoming-events.ui b/plugins/upcoming-events/upcoming-events.ui new file mode 100644 index 000000000..33c64ad37 --- /dev/null +++ b/plugins/upcoming-events/upcoming-events.ui @@ -0,0 +1,58 @@ + + + + + diff --git a/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-acquiring-symbolic.svg b/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-acquiring-symbolic.svg new file mode 100644 index 000000000..cf0e8b318 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-acquiring-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-disabled-symbolic.svg b/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-disabled-symbolic.svg new file mode 100644 index 000000000..20e9225a5 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/icons/network-wireless-hotspot-disabled-symbolic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/wifi-hotspot-quick-setting/icons/wifi-hotspot-quick-setting-symbolic.svg b/plugins/wifi-hotspot-quick-setting/icons/wifi-hotspot-quick-setting-symbolic.svg new file mode 100644 index 000000000..0a68d6027 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/icons/wifi-hotspot-quick-setting-symbolic.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/plugins/wifi-hotspot-quick-setting/meson.build b/plugins/wifi-hotspot-quick-setting/meson.build new file mode 100644 index 000000000..2b7f556cf --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/meson.build @@ -0,0 +1,44 @@ +name = 'wifi-hotspot-quick-setting' + +qrcodegen_dep = dependency('qrcodegen') + +wifi_hotspot_quick_setting_resources = gnome.compile_resources( + 'phosh-plugin-wifi-hotspot-quick-setting-resources', + 'phosh-plugin-wifi-hotspot-quick-setting.gresources.xml', + c_name: 'phosh_plugin_wifi_hotspot_quick_setting', +) + +wifi_hotspot_quick_setting_plugin_sources = files( + 'phosh-plugin-wifi-hotspot-quick-setting.c', + 'wifi-hotspot-quick-setting.c', + 'wifi-hotspot-status-page.c', +) + +phosh_wifi_hotspot_quick_setting_plugin = shared_module( + 'phosh-plugin-wifi-hotspot-quick-setting', + wifi_hotspot_quick_setting_plugin_sources, + wifi_hotspot_quick_setting_resources, + c_args: ['-DG_LOG_DOMAIN="phosh-plugin-@0@"'.format(name), '-DPLUGIN_NAME="@0@"'.format(name)], + dependencies: [plugin_dep, qrcodegen_dep], + install: true, + install_dir: plugins_dir, +) + +pluginconf = configuration_data() +pluginconf.set('name', name) +pluginconf.set('plugins_dir', plugins_dir) + +i18n.merge_file( + input: configure_file( + input: name + '.desktop.in.in', + output: name + '.desktop.in', + configuration: pluginconf, + ), + output: name + '.plugin', + po_dir: join_paths(meson.project_source_root(), 'po'), + install: true, + install_dir: plugins_dir, + type: 'desktop', +) + +install_data('icons/wifi-hotspot-quick-setting-symbolic.svg', install_dir: phosh_plugin_icon_dir) diff --git a/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.c b/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.c new file mode 100644 index 000000000..689338d5f --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.c @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "phosh-plugin.h" +#include "wifi-hotspot-quick-setting.h" + + +char **g_io_phosh_plugin_wifi_hotspot_quick_setting_query (void); + + +void +g_io_module_load (GIOModule *module) +{ + g_type_module_use (G_TYPE_MODULE (module)); + + g_io_extension_point_implement (PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, + PHOSH_TYPE_WIFI_HOTSPOT_QUICK_SETTING, + PLUGIN_NAME, + 10); +} + + +void +g_io_module_unload (GIOModule *module) +{ +} + + +char ** +g_io_phosh_plugin_wifi_hotspot_quick_setting_query (void) +{ + char *extension_points[] = {PHOSH_PLUGIN_EXTENSION_POINT_QUICK_SETTING_WIDGET, NULL}; + + return g_strdupv (extension_points); +} diff --git a/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.gresources.xml b/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.gresources.xml new file mode 100644 index 000000000..17c1e5787 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/phosh-plugin-wifi-hotspot-quick-setting.gresources.xml @@ -0,0 +1,10 @@ + + + + status-page.ui + qs.ui + icons/network-wireless-hotspot-disabled-symbolic.svg + icons/network-wireless-hotspot-acquiring-symbolic.svg + style.css + + diff --git a/plugins/wifi-hotspot-quick-setting/qs.ui b/plugins/wifi-hotspot-quick-setting/qs.ui new file mode 100644 index 000000000..039c27254 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/qs.ui @@ -0,0 +1,14 @@ + + + + + + 1 + 16 + + + diff --git a/plugins/wifi-hotspot-quick-setting/status-page.ui b/plugins/wifi-hotspot-quick-setting/status-page.ui new file mode 100644 index 000000000..5627360f5 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/status-page.ui @@ -0,0 +1,89 @@ + + + + + + 1 + crossfade + 0 + + + 1 + turn_on_btn + + + empty_state + + + + + 1 + vertical + 6 + + + 1 + 1 + end + center + + + + + + 1 + 128 + + + + + 1 + entry + center + 0.5 + 0 + password + 0 + view-reveal-symbolic + + + + + + + hotspot_enabled + + + + + 1 + center + Turn On + 0 + + + + 1 + 1 + 0 + panel.launch-panel + ("wifi", [<"">]) + + + 1 + end + Wi-Fi Settings + + + + diff --git a/plugins/wifi-hotspot-quick-setting/style.css b/plugins/wifi-hotspot-quick-setting/style.css new file mode 100644 index 000000000..79728dfd5 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/style.css @@ -0,0 +1,5 @@ +phosh-wifi-hotspot-status-page .password-entry, +phosh-wifi-hotspot-status-page .ssid { + font-size: 1.25rem; + font-weight: bold; +} diff --git a/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c new file mode 100644 index 000000000..85b7b07df --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "wifi-hotspot-quick-setting.h" +#include "wifi-hotspot-status-page.h" +#include "plugin-shell.h" + +#include + +/** + * PhoshWifiHotspotQuickSetting: + * + * Toggle Wi-Fi hotspot. + */ + +struct _PhoshWifiHotspotQuickSetting { + PhoshQuickSetting parent; + + PhoshStatusIcon *info; + gboolean connecting; + PhoshWifiManager *wifi; +}; + +G_DEFINE_TYPE (PhoshWifiHotspotQuickSetting, phosh_wifi_hotspot_quick_setting, + PHOSH_TYPE_QUICK_SETTING); + + +static void +on_clicked (PhoshWifiHotspotQuickSetting *self) +{ + gboolean active; + + self->connecting = TRUE; + active = phosh_quick_setting_get_active (PHOSH_QUICK_SETTING (self)); + phosh_wifi_manager_set_hotspot_master (self->wifi, !active); + + if (active) + g_signal_emit_by_name (self, "hide-status", 0); +} + + +static void +update_sensitivity_cb (PhoshWifiHotspotQuickSetting *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + gboolean sensitive; + + if (phosh_shell_get_locked (shell)) { + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); + return; + } + + sensitive = phosh_wifi_manager_get_enabled (self->wifi); + gtk_widget_set_sensitive (GTK_WIDGET (self), sensitive); +} + + +static void +update_info_cb (PhoshWifiHotspotQuickSetting *self) +{ + gboolean wifi_enabled; + gboolean hotspot_enabled; + const char *info; + const char *icon_name; + NMActiveConnectionState state; + + wifi_enabled = phosh_wifi_manager_get_enabled (self->wifi); + hotspot_enabled = phosh_wifi_manager_is_hotspot_master (self->wifi); + state = phosh_wifi_manager_get_state (self->wifi); + + g_debug ("State: %d, Hotspot: %d Wi-Fi: %d", state, hotspot_enabled, wifi_enabled); + + /* Translators: A Wi-Fi hotspot */ + if (hotspot_enabled) + info = _("Hotspot On"); + else + info = _("Hotspot Off"); + + /* Show the acquiring icon only if the state change occurred + * through user clicking the quick setting */ + if (self->connecting && + (state == NM_ACTIVE_CONNECTION_STATE_ACTIVATING || + state == NM_ACTIVE_CONNECTION_STATE_DEACTIVATING)) { + icon_name = "network-wireless-hotspot-acquiring-symbolic"; + } else { + if (hotspot_enabled) + icon_name = "network-wireless-hotspot-symbolic"; + else + icon_name = "network-wireless-hotspot-disabled-symbolic"; + self->connecting = FALSE; + } + + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self->info), info); + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self->info), icon_name); +} + + +static void +phosh_wifi_hotspot_quick_setting_class_init (PhoshWifiHotspotQuickSettingClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + g_type_ensure (PHOSH_TYPE_WIFI_HOTSPOT_STATUS_PAGE); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/wifi-hotspot-quick-setting/qs.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotQuickSetting, info); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked); +} + + +static void +phosh_wifi_hotspot_quick_setting_init (PhoshWifiHotspotQuickSetting *self) +{ + g_autoptr (GtkCssProvider) css_provider = NULL; + PhoshShell *shell = phosh_shell_get_default (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + css_provider = gtk_css_provider_new (); + gtk_css_provider_load_from_resource (css_provider, + "/mobi/phosh/plugins/wifi-hotspot-quick-setting/style.css"); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (css_provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), + "/mobi/phosh/plugins/wifi-hotspot-quick-setting/icons"); + + self->wifi = phosh_shell_get_wifi_manager (shell); + if (self->wifi == NULL) { + g_warning ("Failed to get Wi-Fi manager"); + return; + } + + g_object_bind_property (self->wifi, "is-hotspot-master", + self, "active", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_signal_connect_object (shell, + "notify::locked", + G_CALLBACK (update_sensitivity_cb), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (self->wifi, + "notify::enabled", + G_CALLBACK (update_sensitivity_cb), + self, + G_CONNECT_SWAPPED); + update_sensitivity_cb (self); + + g_signal_connect_object (self->wifi, + "notify::state", + G_CALLBACK (update_info_cb), + self, + G_CONNECT_SWAPPED); + update_info_cb (self); +} diff --git a/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.desktop.in.in b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.desktop.in.in new file mode 100644 index 000000000..55010f643 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.desktop.in.in @@ -0,0 +1,8 @@ +[Plugin] +# Translators: This is an internal id, no need to translate it +Id=@name@ +Name=Wi-Fi Hotspot Quick Setting +Types=quick-setting; +Comment=Toggle Wi-Fi hotspot on/off +Plugin=@plugins_dir@/libphosh-plugin-@name@.so +Icon=wifi-hotspot-quick-setting diff --git a/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.h b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.h new file mode 100644 index 000000000..63ad4b4fe --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#pragma once + +#include "quick-setting.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_WIFI_HOTSPOT_QUICK_SETTING phosh_wifi_hotspot_quick_setting_get_type () + +G_DECLARE_FINAL_TYPE (PhoshWifiHotspotQuickSetting, + phosh_wifi_hotspot_quick_setting, + PHOSH, WIFI_HOTSPOT_QUICK_SETTING, PhoshQuickSetting) + +G_END_DECLS diff --git a/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.c b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.c new file mode 100644 index 000000000..37db015d2 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.c @@ -0,0 +1,421 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#include "plugin-shell.h" +#include "status-page-placeholder.h" +#include "wifi-hotspot-status-page.h" + +#include + +#define QR_CODE_SIZE 128 + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_t, cairo_destroy) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_surface_t, cairo_surface_destroy) + +/** + * PhoshWifiHotspotStatusPage: + * + * A status-page to show password of Wi-Fi hotspot. + * + * The code to display QR is taken from GNOME Control Center. + */ + +struct _PhoshWifiHotspotStatusPage { + PhoshStatusPage parent; + + GtkEntry *entry; + GtkImage *image; + PhoshStatusPagePlaceholder *placeholder; + GtkLabel *ssid; + GtkStack *stack; + GtkButton *turn_on_btn; + + GCancellable *cancel; + PhoshWifiManager *wifi; +}; + +G_DEFINE_TYPE (PhoshWifiHotspotStatusPage, phosh_wifi_hotspot_status_page, PHOSH_TYPE_STATUS_PAGE); + + +static cairo_surface_t * +qr_from_text (const char *text, int size, int scale) +{ + uint8_t qr_code[qrcodegen_BUFFER_LEN_FOR_VERSION (qrcodegen_VERSION_MAX)]; + uint8_t temp_buf[qrcodegen_BUFFER_LEN_FOR_VERSION (qrcodegen_VERSION_MAX)]; + g_autoptr (cairo_t) cr = NULL; + cairo_surface_t *surface; + int pixel_size, qr_size, padding; + gboolean success = FALSE; + + g_return_val_if_fail (size > 0, NULL); + g_return_val_if_fail (scale > 0, NULL); + + success = qrcodegen_encodeText (text, + temp_buf, + qr_code, + qrcodegen_Ecc_LOW, + qrcodegen_VERSION_MIN, + qrcodegen_VERSION_MAX, + qrcodegen_Mask_AUTO, + FALSE); + + if (!success) + return NULL; + + surface = cairo_image_surface_create (CAIRO_FORMAT_RGB24, size * scale, size * scale); + cairo_surface_set_device_scale (surface, scale, scale); + cr = cairo_create (surface); + cairo_set_antialias (cr, CAIRO_ANTIALIAS_NONE); + + /* Draw white background */ + cairo_set_source_rgba (cr, 1, 1, 1, 1); + cairo_rectangle (cr, 0, 0, size * scale, size * scale); + cairo_fill (cr); + + qr_size = qrcodegen_getSize (qr_code); + pixel_size = MAX (1, size / (qr_size)); + padding = (size - qr_size * pixel_size) / 2; + + /* If subpixel size is big and margin is pretty small, + * increase the margin */ + if (pixel_size > 4 && padding < 12) { + pixel_size--; + padding = (size - qr_size * pixel_size) / 2; + } + + /* Now draw the black QR code pixels */ + cairo_set_source_rgba (cr, 0, 0, 0, 1); + for (int row = 0; row < qr_size; row++) { + for (int column = 0; column < qr_size; column++) { + if (qrcodegen_getModule (qr_code, row, column)) { + cairo_rectangle (cr, + column * pixel_size + padding, + row * pixel_size + padding, + pixel_size, pixel_size); + cairo_fill (cr); + } + } + } + + return surface; +} + + +static char * +escape_string (const char *str, gboolean quote) +{ + GString *string; + const char *next; + + if (!str) + return NULL; + + string = g_string_new (""); + if (quote) + g_string_append_c (string, '"'); + + while ((next = strpbrk (str, "\\;,:\""))) { + g_string_append_len (string, str, next - str); + g_string_append_c (string, '\\'); + g_string_append_c (string, *next); + str = next + 1; + } + + g_string_append (string, str); + if (quote) + g_string_append_c (string, '"'); + + return g_string_free (string, FALSE); +} + + +static const char * +get_connection_security_type (NMConnection *c) +{ + NMSettingWirelessSecurity *setting; + const char *key_mgmt; + + g_return_val_if_fail (c, "nopass"); + + setting = nm_connection_get_setting_wireless_security (c); + + if (!setting) + return "nopass"; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt (setting); + + /* No IEEE 802.1x */ + if (g_strcmp0 (key_mgmt, "none") == 0) + return "WEP"; + + if (g_strcmp0 (key_mgmt, "wpa-psk") == 0) + return "WPA"; + + if (g_strcmp0 (key_mgmt, "sae") == 0) + return "SAE"; + + return "nopass"; +} + + +static char * +get_wifi_password (NMConnection *c) +{ + NMSettingWirelessSecurity *setting; + const char *sec_type, *password; + int wep_index; + + sec_type = get_connection_security_type (c); + setting = nm_connection_get_setting_wireless_security (c); + + if (g_str_equal (sec_type, "nopass")) + return NULL; + + if (g_str_equal (sec_type, "WEP")) { + wep_index = nm_setting_wireless_security_get_wep_tx_keyidx (setting); + password = nm_setting_wireless_security_get_wep_key (setting, wep_index); + } else { + password = nm_setting_wireless_security_get_psk (setting); + } + + return g_strdup (password); +} + + +/* Generate a string representing the connection + * An example generated text: + * WIFI:S:ssid;T:WPA;P:my-valid-pass;H:true; + * Where, + * S = ssid, T = security, P = password, H = hidden (Optional) + * + * See https://github.com/zxing/zxing/wiki/Barcode-Contents#wi-fi-network-config-android-ios-11 + */ +static char * +get_qr_string_for_connection (NMConnection *c) +{ + NMSettingWireless *setting; + g_autofree char *ssid_text = NULL; + g_autofree char *escaped_ssid = NULL; + g_autofree char *password_str = NULL; + g_autofree char *escaped_password = NULL; + GString *string; + GBytes *ssid; + gboolean hidden; + + setting = nm_connection_get_setting_wireless (c); + ssid = nm_setting_wireless_get_ssid (setting); + + if (!ssid) + return NULL; + + string = g_string_new ("WIFI:S:"); + + /* SSID */ + ssid_text = nm_utils_ssid_to_utf8 (g_bytes_get_data (ssid, NULL), + g_bytes_get_size (ssid)); + escaped_ssid = escape_string (ssid_text, FALSE); + g_string_append (string, escaped_ssid); + g_string_append_c (string, ';'); + + /* Security type */ + g_string_append (string, "T:"); + g_string_append (string, get_connection_security_type (c)); + g_string_append_c (string, ';'); + + /* Password */ + g_string_append (string, "P:"); + password_str = get_wifi_password (c); + escaped_password = escape_string (password_str, FALSE); + if (escaped_password) + g_string_append (string, escaped_password); + g_string_append_c (string, ';'); + + /* WiFi Hidden */ + hidden = nm_setting_wireless_get_hidden (setting); + if (hidden) + g_string_append (string, "H:true"); + g_string_append_c (string, ';'); + + return g_string_free (string, FALSE); +} + + +static void +on_secrets_ready (GObject *object, GAsyncResult *result, gpointer data) +{ + PhoshWifiHotspotStatusPage *self = data; + NMConnection *conn = NM_CONNECTION (object); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) variant = NULL; + g_autofree char *cnx_str = NULL; + g_autofree char *password = NULL; + g_autoptr (cairo_surface_t) surface = NULL; + int scale; + + variant = nm_remote_connection_get_secrets_finish (NM_REMOTE_CONNECTION (conn), result, &error); + if (variant == NULL) { + g_warning ("Unable to fetch secrets: %s", error->message); + gtk_image_set_from_icon_name (self->image, "face-sad-symbolic", -1); + gtk_entry_set_text (self->entry, ""); + return; + } + if (!nm_connection_update_secrets (conn, + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + variant, + &error)) { + g_warning ("Unable to set secrets: %s", error->message); + gtk_image_set_from_icon_name (self->image, "face-sad-symbolic", -1); + gtk_entry_set_text (self->entry, ""); + return; + } + + cnx_str = get_qr_string_for_connection (conn); + scale = gtk_widget_get_scale_factor (GTK_WIDGET (self->image)); + surface = qr_from_text (cnx_str, QR_CODE_SIZE, scale); + password = get_wifi_password (conn); + gtk_image_set_from_surface (self->image, surface); + gtk_entry_set_text (self->entry, password); + nm_connection_clear_secrets (conn); +} + + +static void +setup_hotspot_page (PhoshWifiHotspotStatusPage *self) +{ + NMActiveConnection *conn = phosh_wifi_manager_get_active_connection (self->wifi); + NMRemoteConnection *remote = nm_active_connection_get_connection (conn); + + nm_remote_connection_get_secrets_async (NM_REMOTE_CONNECTION (remote), + NM_SETTING_WIRELESS_SECURITY_SETTING_NAME, + self->cancel, + on_secrets_ready, + self); +} + + +static void +on_wifi_notify (PhoshWifiHotspotStatusPage *self) +{ + gboolean wifi_absent = !phosh_wifi_manager_get_present (self->wifi); + gboolean wifi_disabled = !phosh_wifi_manager_get_enabled (self->wifi); + gboolean hotspot_disabled = !phosh_wifi_manager_is_hotspot_master (self->wifi); + const char *icon_name; + + if (wifi_absent) + icon_name = "network-wireless-hardware-disabled-symbolic"; + else if (wifi_disabled) + icon_name = "network-wireless-disabled-symbolic"; + else + icon_name = "network-wireless-hotspot-disabled-symbolic"; + + phosh_status_page_placeholder_set_icon_name (self->placeholder, icon_name); + gtk_widget_set_visible (GTK_WIDGET (self->turn_on_btn), !wifi_absent); + + if (hotspot_disabled) { + gtk_stack_set_visible_child_name (self->stack, "empty_state"); + } else { + gtk_stack_set_visible_child_name (self->stack, "hotspot_enabled"); + setup_hotspot_page (self); + } +} + + +static void +on_icon_press (PhoshWifiHotspotStatusPage *self) +{ + gboolean visibility = gtk_entry_get_visibility (self->entry); + const char *icon_name; + + if (visibility) + icon_name = "view-reveal-symbolic"; + else + icon_name = "view-conceal-symbolic"; + + gtk_entry_set_visibility (self->entry, !visibility); + gtk_entry_set_icon_from_icon_name (self->entry, GTK_ENTRY_ICON_SECONDARY, icon_name); +} + + +static void +on_turn_on_clicked (PhoshWifiHotspotStatusPage *self) +{ + gboolean wifi_disabled = !phosh_wifi_manager_get_enabled (self->wifi); + + if (wifi_disabled) + phosh_wifi_manager_set_enabled (self->wifi, TRUE); + else + phosh_wifi_manager_set_hotspot_master (self->wifi, TRUE); +} + + +static void +phosh_wifi_hotspot_status_page_dispose (GObject *object) +{ + PhoshWifiHotspotStatusPage *self = PHOSH_WIFI_HOTSPOT_STATUS_PAGE (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (self->wifi) + g_signal_handlers_disconnect_by_data (self->wifi, self); + + G_OBJECT_CLASS (phosh_wifi_hotspot_status_page_parent_class)->dispose (object); +} + + +static void +phosh_wifi_hotspot_status_page_class_init (PhoshWifiHotspotStatusPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_wifi_hotspot_status_page_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/plugins/wifi-hotspot-quick-setting/status-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, entry); + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, image); + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, placeholder); + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, ssid); + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, stack); + gtk_widget_class_bind_template_child (widget_class, PhoshWifiHotspotStatusPage, turn_on_btn); + gtk_widget_class_bind_template_callback (widget_class, on_turn_on_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_icon_press); + + gtk_widget_class_set_css_name (widget_class, "phosh-wifi-hotspot-status-page"); +} + + +static void +phosh_wifi_hotspot_status_page_init (PhoshWifiHotspotStatusPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->cancel = g_cancellable_new (); + self->wifi = phosh_shell_get_wifi_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_WIFI_MANAGER (self->wifi)); + + g_object_connect (self->wifi, + "swapped-object-signal::notify::present", + G_CALLBACK (on_wifi_notify), self, + "swapped-object-signal::notify::enabled", + G_CALLBACK (on_wifi_notify), self, + "swapped-object-signal::notify::is-hotspot-master", + G_CALLBACK (on_wifi_notify), self, + NULL); + g_object_bind_property (self->wifi, "ssid", self->ssid, "label", G_BINDING_SYNC_CREATE); +} + + +GtkWidget * +phosh_wifi_hotspot_status_page_new (void) +{ + return g_object_new (PHOSH_TYPE_WIFI_HOTSPOT_STATUS_PAGE, NULL); +} diff --git a/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.h b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.h new file mode 100644 index 000000000..21fadc497 --- /dev/null +++ b/plugins/wifi-hotspot-quick-setting/wifi-hotspot-status-page.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "status-page.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_WIFI_HOTSPOT_STATUS_PAGE phosh_wifi_hotspot_status_page_get_type () + +G_DECLARE_FINAL_TYPE (PhoshWifiHotspotStatusPage, phosh_wifi_hotspot_status_page, PHOSH, + WIFI_HOTSPOT_STATUS_PAGE, PhoshStatusPage) + +GtkWidget *phosh_wifi_hotspot_status_page_new (void); + +G_END_DECLS diff --git a/po/LINGUAS b/po/LINGUAS new file mode 100644 index 000000000..e156bcf31 --- /dev/null +++ b/po/LINGUAS @@ -0,0 +1,51 @@ +am +ar +be +bg +ca +cs +da +de +el +en_GB +eo +es +eu +fa +fi +fr +fur +fy +gl +he +hi +hr +ht +hu +id +it +ja +ka +ko +kw +la +lv +nb +nl +oc +pl +pt +pt_BR +ro +ru +sat +sk +sl +sr +sv +tr +uk +uz +zh_CN +zh_Hans_CN +zh_TW diff --git a/po/POTFILES.in b/po/POTFILES.in new file mode 100644 index 000000000..e1b02d6cd --- /dev/null +++ b/po/POTFILES.in @@ -0,0 +1,146 @@ +# Desktop files +data/mobi.phosh.Shell.desktop.in.in +data/phosh.session.desktop.in.in +data/wayland-sessions/phosh.desktop +plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in +plugins/calendar/calendar.desktop.in.in +plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in +plugins/emergency-info/emergency-info.desktop.in.in +plugins/launcher-box/launcher-box.desktop.in.in +plugins/location-quick-setting/location-quick-setting.desktop.in.in +plugins/media-players/media-players.desktop.in.in +plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in +plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in +plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in +plugins/ticket-box/ticket-box.desktop.in.in +plugins/upcoming-events/upcoming-events.desktop.in.in + +# Source files +src/activity.c +src/app-grid-button.c +src/app-grid.c +src/app-list-model.c +src/arrow.c +src/audio-manager.c +src/auth.c +src/background.c +src/background-manager.c +src/battery-info.c +src/bt-device-row.c +src/bt-info.c +src/bt-status-page.c +src/call-notification.c +src/connectivity-manager.c +src/docked-info.c +src/emergency-menu.c +src/end-session-dialog.c +src/fader.c +src/favorite-list-model.c +src/feedbackinfo.c +src/feedback-manager.c +src/home.c +src/idle-manager.c +src/layersurface.c +src/location-manager.c +src/lockscreen.c +src/lockscreen-manager.c +src/screenshot-manager.c +src/lockshield.c +src/main.c +src/media-player.c +src/monitor-manager.c +src/network-auth-prompt.c +src/notifications/mount-notification.c +src/notifications/notification.c +src/notifications/notify-manager.c +src/notifications/timestamp-label.c +src/osk-manager.c +src/overview.c +src/phosh-wayland.c +src/polkit-auth-agent.c +src/polkit-auth-prompt.c +src/proximity.c +src/quick-setting.c +src/rotateinfo.c +src/run-command-dialog.c +src/run-command-manager.c +src/screen-saver-manager.c +src/sensor-proxy-manager.c +src/session-presence.c +src/settings.c +src/settings/audio-settings.c +src/shell.c +src/status-icon.c +src/system-prompt.c +src/system-prompter.c +src/toplevel.c +src/toplevel-manager.c +src/torch-info.c +src/ui/activity.ui +src/ui/app-auth-prompt.ui +src/ui/app-grid-button.ui +src/ui/app-grid.ui +src/ui/audio-settings.ui +src/ui/brightness-settings.ui +src/ui/bt-status-page.ui +src/ui/cell-broadcast-prompt.ui +src/ui/emergency-menu.ui +src/ui/emergency-contact-row.ui +src/ui/end-session-dialog.ui +src/ui/feedback-status-page.ui +src/ui/gtk-mount-prompt.ui +src/ui/home.ui +src/ui/lockscreen.ui +src/ui/media-player.ui +src/ui/network-auth-prompt.ui +src/ui/notification-content.ui +src/ui/notification-frame.ui +src/ui/overview.ui +src/ui/polkit-auth-prompt.ui +src/ui/power-menu.ui +src/ui/quick-setting.ui +src/ui/run-command-dialog.ui +src/ui/settings.ui +src/ui/system-prompt.ui +src/ui/top-panel.ui +src/ui/wifi-status-page.ui +src/util.c +src/vpn-info.c +src/wall-clock.c +src/widget-box.c +src/wifi-info.c +src/wifi-manager.c +src/wifi-status-page.c +src/wwan-info.c +plugins/caffeine-quick-setting/caffeine-quick-setting.c +plugins/caffeine-quick-setting/qs.ui +plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c +plugins/caffeine-quick-setting/prefs/prefs.ui +plugins/dark-mode-quick-setting/dark-mode-quick-setting.c +plugins/emergency-info/emergency-info.c +plugins/emergency-info/emergency-info.ui +plugins/emergency-info/prefs/emergency-info-prefs.c +plugins/emergency-info/prefs/emergency-info-prefs.ui +plugins/emergency-info/prefs/emergency-info-prefs-row.c +plugins/emergency-info/prefs/emergency-info-prefs-row.ui +plugins/launcher-box/launcher-box.ui +plugins/location-quick-setting/location-quick-setting.c +plugins/media-players/media-players.ui +plugins/mobile-data-quick-setting/mobile-data-quick-setting.c +plugins/night-light-quick-setting/night-light-quick-setting.c +plugins/pomodoro-quick-setting/pomodoro-quick-setting.c +plugins/pomodoro-quick-setting/prefs/prefs.ui +plugins/ticket-box/ticket-box.ui +plugins/ticket-box/prefs/ticket-box-prefs.c +plugins/ticket-box/prefs/ticket-box-prefs.ui +plugins/upcoming-events/event-list.c +plugins/upcoming-events/event-list.ui +plugins/upcoming-events/upcoming-event.c +plugins/upcoming-events/upcoming-events.c +plugins/upcoming-events/upcoming-events.ui +plugins/upcoming-events/prefs/upcoming-events-prefs.ui +plugins/scaling-quick-setting/qs.ui +plugins/scaling-quick-setting/scale-row.c +plugins/scaling-quick-setting/scaling-quick-setting.c +plugins/wifi-hotspot-quick-setting/status-page.ui +plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c diff --git a/po/POTFILES.skip b/po/POTFILES.skip new file mode 100644 index 000000000..bd7f35ce0 --- /dev/null +++ b/po/POTFILES.skip @@ -0,0 +1,7 @@ +src/gtk-list-models/gtkfilterlistmodel.c +src/gtk-list-models/gtksortlistmodel.c +src/settings/channel-bar.c +subprojects/ +plugins/simple-custom-quick-setting/qs.ui +plugins/simple-custom-quick-setting/simple-custom-quick-setting.c +plugins/simple-custom-quick-setting/simple-custom-quick-setting.desktop.in.in diff --git a/po/am.po b/po/am.po new file mode 100644 index 000000000..4a571e1db --- /dev/null +++ b/po/am.po @@ -0,0 +1,145 @@ +# Samson , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-14 13:18+0100\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-02-17 05:00+0000\n" +"Last-Translator: Samson \n" +"Language-Team: Amharic\n" +"Language: am\n" +"X-Generator: Zanata 4.6.2\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "" + +#: src/app-grid-button.c:523 +msgid "Application" +msgstr "" + +#: src/feedbackinfo.c:38 +msgid "Quiet" +msgstr "" + +#: src/feedbackinfo.c:40 +msgid "Silent" +msgstr "" + +#: src/feedbackinfo.c:42 +msgid "On" +msgstr "" + +#: src/lockscreen.c:78 src/ui/lockscreen.ui:204 +msgid "Enter Passcode" +msgstr "" + +#: src/lockscreen.c:257 +msgid "Checking…" +msgstr "" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:334 +msgid "%A, %B %-e" +msgstr "" + +#: src/monitor-manager.c:53 +msgid "Built-in display" +msgstr "" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:57 +msgid "Unknown" +msgstr "ያልታወቀ" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "" + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "" + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "የ መግቢያ ቃል አይመሳሰልም" + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "የ መግቢያ ቃል ባዶ መሆን የለበትም" + +#: src/wifiinfo.c:55 +msgid "Wi-Fi" +msgstr "" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "" + +#: src/ui/lockscreen.ui:36 +msgid "Slide up to unlock" +msgstr "ለ መክፈት ይግለጹ" + +#: src/ui/lockscreen.ui:250 +msgid "Emergency" +msgstr "" + +#: src/ui/lockscreen.ui:266 +msgid "Unlock" +msgstr "" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "" diff --git a/po/ar.po b/po/ar.po new file mode 100644 index 000000000..8e98f0d0d --- /dev/null +++ b/po/ar.po @@ -0,0 +1,146 @@ +# Mohamed Lebsir , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-14 13:18+0100\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-11-20 08:43+0000\n" +"Last-Translator: Mohamed Lebsir \n" +"Language-Team: Arabic\n" +"Language: ar\n" +"X-Generator: Zanata 4.6.2\n" +"Plural-Forms: nplurals=6; plural= n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "" + +#: src/app-grid-button.c:523 +msgid "Application" +msgstr "تطبيق" + +#: src/feedbackinfo.c:38 +msgid "Quiet" +msgstr "" + +#: src/feedbackinfo.c:40 +msgid "Silent" +msgstr "" + +#: src/feedbackinfo.c:42 +msgid "On" +msgstr "" + +#: src/lockscreen.c:78 src/ui/lockscreen.ui:204 +msgid "Enter Passcode" +msgstr "أدخل كلمة السر" + +#: src/lockscreen.c:257 +msgid "Checking…" +msgstr "" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:334 +msgid "%A, %B %-e" +msgstr "" + +#: src/monitor-manager.c:53 +msgid "Built-in display" +msgstr "" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:57 +msgid "Unknown" +msgstr "" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "" + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "كلمة السر" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "" + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "" + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "" + +#: src/wifiinfo.c:55 +msgid "Wi-Fi" +msgstr "" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "" + +#: src/ui/lockscreen.ui:36 +msgid "Slide up to unlock" +msgstr "" + +#: src/ui/lockscreen.ui:250 +msgid "Emergency" +msgstr "" + +#: src/ui/lockscreen.ui:266 +msgid "Unlock" +msgstr "" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "مستخدم:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "تأكيد:" diff --git a/po/be.po b/po/be.po new file mode 100644 index 000000000..6b4a91b07 --- /dev/null +++ b/po/be.po @@ -0,0 +1,1178 @@ +# Belarusian translation for phosh. +# Copyright (C) 2023 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Yuras Shumovich , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-03-30 13:03+0000\n" +"PO-Revision-Date: 2025-03-30 23:16+0300\n" +"Last-Translator: Yuras Shumovich \n" +"Language-Team: Belarusian \n" +"Language: be\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.5\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Абалонка тэлефона" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Кіраванне вокнамі і запуск праграм для мабільных прылад" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Гэты сеанс рэгіструе вас у Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Хуткія налады Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Прадухіляе пераход у рэжым сну" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Каляндар" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Просты віджэт календара" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Хуткія налады цёмнага рэжыму, колеравай схемы" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Пераключыць цёмны рэжым" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Звесткі для экстраннай дапамогі" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Паказ звестак кантактаў для экстраннай дапамогі" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Запуск праграм і функцый" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Дадаванне сродкаў запуску праграм і функцый на экран блакіравання. Гэта " +"эксперыментальны плагін." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Хуткія налады перадачы мабільных даных" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Укл/Выкл мабільныя даныя" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Хуткія налады начнога святла" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Укл/Выкл начное святло" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Хуткія налады Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Просты таймер Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Скрыня для білетаў" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Паказваць дакументы PDF на экране блакіравання. Гэта эксперыментальны " +"плагін." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Надыходзячыя падзеі" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Паказваць надыходзячыя падзеі календара" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Дадаць у папку" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Стварыць новую папку" + +#: src/app-grid-button.c:698 src/app-grid-button.c:754 +msgid "Application" +msgstr "Праграма" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Паказваць усе праграмы" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Паказваць толькі праграмы для мабільных прылад" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Батарэя %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Укл" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Падлучальных прылад Bluetooth не знойдзена" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth адключаны" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Невядомы абанент" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Сетка Wi-Fi «%s» выкарыстоўвае партал аўтарызацыі" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Сетка Wi-Fi выкарыстоўвае партал аўтарызацыі" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Увайдзіце ў сетку Wi–Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Прымацавана" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Адмацавана" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:22 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Добра" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Немагчыма выканаць экстранны выклік" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Унутраная памылка" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Выйсці" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s аўтаматычна выйдзе праз %d секунду." +msgstr[1] "%s аўтаматычна выйдзе праз %d секунды." +msgstr[2] "%s аўтаматычна выйдзе праз %d секунд." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:69 +msgid "Power Off" +msgstr "Выключыць" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Сістэма аўтаматычна выключыцца праз %d секунду." +msgstr[1] "Сістэма аўтаматычна выключыцца праз %d секунды." +msgstr[2] "Сістэма аўтаматычна выключыцца праз %d секунд." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Перазапусціць" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Сістэма аўтаматычна перазапусціцца праз %d секунду." +msgstr[1] "Сістэма аўтаматычна перазапусціцца праз %d секунды." +msgstr[2] "Сістэма аўтаматычна перазапусціцца праз %d секунд." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Ціхі" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Бязгучны" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Укл" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Даць праграме %s доступ да вашага месцазнаходжання?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Геалакацыя" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Так" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Не" + +#. give visual feedback on error +#: src/lockscreen.c:311 src/ui/lockscreen.ui:280 +msgid "Enter Passcode" +msgstr "Увядзіце код доступу" + +#: src/lockscreen.c:984 +msgid "Checking…" +msgstr "Праверка…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Здымак экрана захаваны ў «%s»" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Не ўдалося захаваць здымак экрана" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:187 +msgid "Screenshot" +msgstr "Здымак экрана" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:527 +msgid "Screenshots" +msgstr "Здымкі экрана" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:547 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Здымак экрана %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:493 src/ui/media-player.ui:211 +msgid "Unknown Title" +msgstr "Невядомая назва" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:501 src/ui/media-player.ui:199 +msgid "Unknown Artist" +msgstr "Невядомы выканаўца" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Убудаваны дысплэй" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Невядомы" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Тып аўтэнтыфікацыі сеткі Wi-Fi «%s» не падтрымліваецца" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Увядзіце пароль да сеткі Wi-Fi «%s»" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Адкрыць" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1013 +msgid "Notification" +msgstr "Апавяшчэнне" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "зараз" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30 с" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 хв" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 хв" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d хв" +msgstr[1] "%d хв" +msgstr[2] "%d хв" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dгадз" +msgstr[1] "~%dгадз" +msgstr[2] "~%dгадз" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 дз" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d дз." +msgstr[1] "%d д." +msgstr[2] "%d дз." + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 мес" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d мес" +msgstr[1] "%d мес" +msgstr[2] "%d мес" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d г." +msgstr[1] "~%dг." +msgstr[2] "~%dг." + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Больш за %d г." +msgstr[1] "Больш за %d г." +msgstr[2] "Больш за %d г." + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Амаль %d г." +msgstr[1] "Амаль %d г." +msgstr[2] "Амаль %d г." + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Аўтэнтыфікацыя адхілена карыстальнікам" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:77 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Пароль:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Не пацверджана. Паўтарыце спробу." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Кніжная" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Альбомная" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Выкл" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Укл" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Націсніце ESC, каб закрыць" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Не ўдалося выканаць «%s»" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Кіраванне гучнасцю абалонкі тэлефона" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Паролі не супадаюць." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Пароль не можа быць пустым" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Ліхтарык" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Запомніць выбар" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Скасаваць" + +#: src/ui/app-grid-button.ui:32 +msgid "Remove from _Favorites" +msgstr "Выдаліць з абраныга" + +#: src/ui/app-grid-button.ui:37 +msgid "Add to _Favorites" +msgstr "Дадаць у абранае" + +#: src/ui/app-grid-button.ui:42 +msgid "View _Details" +msgstr "_Падрабязнасці" + +#: src/ui/app-grid-button.ui:47 +msgid "Uninstall" +msgstr "Выдаліць" + +#: src/ui/app-grid-button.ui:54 +msgid "_Remove from Folder" +msgstr "_Выдаліць з папкі" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Пошук праграм…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Прылады вываду" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Прылады ўводу" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Налады гуку" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Уключыць Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Налады Bluetooth" + +#: src/ui/emergency-menu.ui:24 +msgid "Close the emergency call dialog" +msgstr "Закрыць акно выкліку экстранных службаў" + +#: src/ui/emergency-menu.ui:50 +msgid "Emergency _Contacts" +msgstr "_Кантакты экстранных службаў" + +#: src/ui/emergency-menu.ui:57 +msgid "Go to the emergency contacts page" +msgstr "Перайсці на старонку кантактаў экстранных службаў" + +#: src/ui/emergency-menu.ui:80 +msgid "Go back to the emergency dialpad page" +msgstr "Вярнуцца на старонку набору экстранных службаў" + +#: src/ui/emergency-menu.ui:103 +msgid "Owner unknown" +msgstr "Уладальнік невядомы" + +#: src/ui/emergency-menu.ui:121 plugins/emergency-info/emergency-info.ui:208 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Кантакты экстранных службаў" + +#: src/ui/emergency-menu.ui:139 +msgid "No emergency contacts available." +msgstr "Няма кантактаў экстранных службаў." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Некаторыя праграмы заняты або маюць незахаваныя вынікі працы" + +#: src/ui/gtk-mount-prompt.ui:77 +msgid "User:" +msgstr "Карыстальнік:" + +#: src/ui/gtk-mount-prompt.ui:99 +msgid "Domain:" +msgstr "Дамен:" + +#: src/ui/gtk-mount-prompt.ui:131 +msgid "Co_nnect" +msgstr "_Злучыцца" + +#: src/ui/lockscreen.ui:42 src/ui/lockscreen.ui:243 +msgid "Back" +msgstr "Назад" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Правядзіце пальцам, каб разблакіраваць" + +#: src/ui/lockscreen.ui:330 +msgid "Unlock" +msgstr "Разблакіраваць" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Патрабуецца аўтэнтыфікацыя" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Скасаваць" + +#: src/ui/network-auth-prompt.ui:54 +msgid "C_onnect" +msgstr "_Злучыцца" + +#: src/ui/polkit-auth-prompt.ui:100 +msgid "Authenticate" +msgstr "Пацвердзіць" + +#: src/ui/power-menu.ui:106 +msgid "Suspend" +msgstr "Прыпыніць" + +#: src/ui/power-menu.ui:149 +msgid "Lock" +msgstr "Заблакіраваць" + +#: src/ui/power-menu.ui:225 +msgid "Emergency" +msgstr "Экстранныя службы" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Выканаць каманду" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Няма апавяшчэнняў" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Апавяшчэнні" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Ачысціць усе" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Пацвярджэнне:" + +#: src/ui/top-panel.ui:31 +msgid "_Power Off…" +msgstr "_Выключыць…" + +#: src/ui/top-panel.ui:58 +msgid "_Restart…" +msgstr "_Перазапусціць…" + +#: src/ui/top-panel.ui:85 +msgid "_Suspend…" +msgstr "_Прыпыніць…" + +#: src/ui/top-panel.ui:112 +msgid "_Log Out…" +msgstr "_Выйсці…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:86 +msgid "Wi-Fi Settings" +msgstr "Налады Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Плагін не знойдзены" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Не ўдалося загрузіць плагін «%s»." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Прылада Wi-Fi не знойдзена" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi адключаны" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Уключыць Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Хот-спот Wi-Fi актыўны" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Выключыць" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Няма хот-спотаў Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Сотавая сетка" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh на caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Укл" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Выкл" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Прадвызначаны рэжым" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Цёмны рэжым" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Светлы рэжым" + +#: plugins/emergency-info/emergency-info.ui:40 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Асабовая інфармацыя" + +#: plugins/emergency-info/emergency-info.ui:48 +msgid "Date of Birth" +msgstr "Дата нараджэння" + +#: plugins/emergency-info/emergency-info.ui:68 +msgid "Preferred Language" +msgstr "Пажаданая мова" + +#: plugins/emergency-info/emergency-info.ui:88 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Дамашні адрас" + +#: plugins/emergency-info/emergency-info.ui:96 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Медыцынскія звесткі" + +#: plugins/emergency-info/emergency-info.ui:104 +msgid "Age" +msgstr "Узрост" + +#: plugins/emergency-info/emergency-info.ui:124 +msgid "Blood Type" +msgstr "Група крыві" + +#: plugins/emergency-info/emergency-info.ui:144 +msgid "Height" +msgstr "Рост" + +#: plugins/emergency-info/emergency-info.ui:164 +msgid "Weight" +msgstr "Вага" + +#: plugins/emergency-info/emergency-info.ui:184 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Алергія" + +#: plugins/emergency-info/emergency-info.ui:192 +msgid "Medications & Conditions" +msgstr "Лекавыя сродкі і захворванні" + +#: plugins/emergency-info/emergency-info.ui:200 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Іншая інфармацыя" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Звесткі для экстраннай дапамогі" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Гатова" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Імя ўладальніка" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Дата нараджэння" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Пажаданая _мова" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Узрост" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Група крыві" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Рост" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Вага" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Лекавыя сродкі і захворванні" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Дадаць кантакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Дадаць новы кантакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Дадаць" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Імя кантакту" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Сваяцтва" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Кантактны _нумар" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Сродкі запуску праграм і функцый не наладжаны" + +#: plugins/launcher-box/launcher-box.ui:30 +msgid "Launchers" +msgstr "Запуск праграм і функцый" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Мабільныя даныя ўключаны" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Мабільныя даныя выключаны" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Начное святло ўключана" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Начное святло выключана" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Pomodoro запушчаны" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Засяродзьцеся на задачы на %d хв" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Зрабіць перапынак" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "У вас %d хв да наступнага сеанса Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Таймер Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +#| msgid "Power Off" +msgid "Pomodoro Off" +msgstr "Pomodoro выкл" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Параметры хуткіх налад Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Метад Pomodoro (памідора)" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Працягласць _дзейнасці" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Працягласць сеанса дзейнасці" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Працягласць _перапынку" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Працягласць перапынку паміж сеансамі" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Няма дакументаў для паказу" + +#: plugins/ticket-box/ticket-box.ui:78 +msgid "Tickets" +msgstr "Білеты" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "Адкрыць" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Выберыце папку" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Параметры скрыні для білетаў" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Шляхі" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Налады папкі" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Дзе Phosh павінен шукаць білеты" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Папка з білетамі" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Сёння" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Заўтра" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "Праз %u день" +msgstr[1] "Праз %u дні" +msgstr[2] "Праз %u дзён" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Няма падзей" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Увесь дзень" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Канец" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Неназваная падзея" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Налады надыходзячых падзей" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Дні" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Прамежак часу" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Падзеі паказваюцца для гэтай колькасці дзён" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Маштаб экрана" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Хот-спот уключаны" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Хот-спот выключаны" + +#~ msgid "Scan" +#~ msgstr "Пошук" + +#~ msgid "Add" +#~ msgstr "Дадаць" + +#~ msgid "Number" +#~ msgstr "Нумар" + +#~ msgid "Unknown application" +#~ msgstr "Невядомая праграма" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "с" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "хв" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "хв" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "гадз" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "гадз" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "мес" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "мес" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "г" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "г" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Праграма" + +#~ msgid "_Power Off" +#~ msgstr "_Выключыць" + +#~ msgid "_Screenshot" +#~ msgstr "Здымак _экрана" diff --git a/po/bg.po b/po/bg.po new file mode 100644 index 000000000..e049eb943 --- /dev/null +++ b/po/bg.po @@ -0,0 +1,1249 @@ +# Bulgarian translation of phosh po-file. +# Copyright (C) 2019 Miro Cekov . +# Copyright (C) 2024, 2025 twlvnn kraftwerk . +# This file is distributed under the same license as the phosh-tour package. +# Miro Cekov , 2019. #zanata +# twlvnn kraftwerk , 2024, 2025, 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-04 20:29+0100\n" +"Last-Translator: twlvnn kraftwerk \n" +"Language-Team: Bulgarian \n" +"Language: bg\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Телефон" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "" +"Управление на прозорци и стартиране на приложения за мобилни устройства" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Тази сесия ви вписва в Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Бърза настройка за безсъние" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Сесията да не заспива при бездействие" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Календар" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Прост графичен обект за календар" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Бърза настройка за цветовата схема" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Превключване на тъмен режим" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Информация за спешни случаи" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Показване на информация и контакти за спешни случаи" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Кутия за стартери" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Добавя стартери на заключения екран. Тази приставка е експериментална." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Бърза настройка за местоположение" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Превключване на услугите за местоположение" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Музикални устройства" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Следете работещите музикални устройства" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Бърза настройка за мобилните данни" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Превключване на мобилните данни" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Бърза настройка за нощния режим" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Превключване на нощен режим" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Бърза настройка за Помодоро" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Прост хронометър за Помодоро" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Кутия за билети" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Показва PDF файлове на заключения екран. Тази приставка е експериментална." +"" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Предстоящи събития" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Показване на предстоящи събития" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Добавяне в папката" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Създаване на нова папка" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Програма" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Показване на всички програми" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Показване само на програмите за мобилни устройства" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Управление на силата на звука" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Батерия %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Вкл." + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Няма намерени Bluetooth устройства за свързване" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth е изключен" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Непознат номер" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wi-Fi мрежата „%s“ използва портал за прихващане" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wi-Fi мрежата използва портал за прихващане" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Влезте в Wi-Fi мрежата" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Скачено" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Откачено" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Добре" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Неуспешно спешно обаждане" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Вътрешна грешка" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Изход" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "" +"Потребителят „%s“ ще излезе от системата автоматично след %d секунда." +msgstr[1] "" +"Потребителят „%s“ ще излезе от системата автоматично след %d секунди." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Изключване" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Системата ще се изключи автоматично след %d секунда." +msgstr[1] "Системата ще се изключи автоматично след %d секунди." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Рестартиране" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Системата ще се рестартира автоматично след %d секунда." +msgstr[1] "Системата ще се рестартира автоматично след %d секунди." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Без звук" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Без звук и вибрации" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Вкл." + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Позволявате ли на „%s“ достъп до информацията за местоположението ви?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Местоположение" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Да" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Не" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Въведете код за достъп" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Проверка…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Снимката на екрана е запазена в „%s“" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Снимката на екрана не може да се запази" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Снимка на екрана" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Снимки на екрана" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Снимка на екрана на %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Неизвестно заглавие" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Неизвестен изпълнител" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Вграден дисплей" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Неизвестен" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Видът на удостоверяване на Wi-Fi мрежата „%s“ не се поддържа" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Въведете паролата за Wi-Fi мрежата „%s“" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Отваряне" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Известие" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "сега" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30с" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1м" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "≈1м" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dм" +msgstr[1] "%dм" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dч" +msgstr[1] "~%dч" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1д" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dд" +msgstr[1] "%dд" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1ме" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dме" +msgstr[1] "%dме" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dг" +msgstr[1] "~%dг" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Над %dг" +msgstr[1] "Над %dг" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Преди %dг" +msgstr[1] "Преди %dг" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Прозорецът за удостоверяване бе затворен от потребителя" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Парола:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "За съжаление това не сработи. Пробвайте наново." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Портрет" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Пейзаж" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Изкл." + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Вкл." + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Натиснете Esc за отмяна" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Неуспешно изпълнение на „%s“" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Паролите не съвпадат." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Паролата не може да бъде празна" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Фенерче" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Запомняне на избора" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Отказване" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Премахване от _любими" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Добавяне в _любими" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Показване на _подробностите" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Деинсталиране" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Премахване от папката" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Търсене на програми…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Изходни устройства" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Входни устройства" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Настройки на звука" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Автоматична яркост" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Настройки за яркост" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Включване на Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Настройки на Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Затваряне на прозореца за спешни обаждания" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Контакти при спешни случаи" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Към страницата с контакти за спешни случаи" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Назад към страницата за набиране на спешни номера" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Неизвестен собственик" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Контакти при спешни случаи" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Няма контакти за спешни случаи." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Някои програми са заети или имат незапазени промени" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Обратна връзка" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Не безпокойте!" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Настройки за обратна връзка" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Потребител:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Домейн:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Свързване" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Назад" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Плъзнете нагоре, за да отключите" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Отключване" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Необходимо е удостоверяване" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Отказване" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Свързване" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Удостоверяване" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Приспиване" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Заключване" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Спешни случаи" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Команда за изпълнение" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Няма известия" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Известия" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Изчистване на всичко" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Потвърждаване:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Изключване…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Рестартиране…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Приспиване…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Изход…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Настройки на Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "ВЧМ (VPN)" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Приставката липсва" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Приставката „%s“ не може да се зареди." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Липсва устройство за Wi-Fi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi е изключен" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Включване на Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Включена е точка за достъп по Wi-Fi" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Изключване" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Няма точки за достъп по Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Клетъчна мрежа" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh на кофеин" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Изкл." + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Вкл." + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Таймери на Caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Няма интервали на Caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Настройка за Caffeine" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Без време за изчакване (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Бързи настройки за Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Продължителност на Caffeine" + +# Няма много място за този текст, затова го съкратих. +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Продължителност" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Добавяне или премахване на собствени интервали на Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Добавяне на интервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Добавяне на нов интервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Добавяне" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Готови интервали" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 м" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 м" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 м" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 ч" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Избор на интервал" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Стил на системата" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Тъмен режим" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Светъл режим" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Лична информация" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Дата на раждане" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Предпочитан език" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Домашен адрес" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Медицинска информация" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Възраст" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Кръвна група" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Височина" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Тегло" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Алергии" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Лекарства и състояния" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Друга информация" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Настройки за информация за спешни случаи" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Готово" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Име на собственика" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Дата на раждане" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Предпочитан _език" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Възраст" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Кръвна група" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Височина" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Тегло" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Лекарства и състояния" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Добавяне на контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Добавяне на нов контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Име за контакта" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Отношения" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Номер за контакта" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Няма настроени стартери" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Стартери" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Местоположението е вкл." + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Местоположението е изкл." + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Няма работещи музикални устройства" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Мобилните данни са вкл." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Мобилните данни са изкл." + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Нощен режим е вкл." + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Нощен режим е изкл." + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Начало на Помодоро" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Фокусирайте се върху задачата си за %d минути" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Вземете си почивка" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Имате %d минути до следващото Помодоро" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Хронометър за Помодоро" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Изключване на Помодоро" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Бързо настройване на Помодоро" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Техника Помодоро" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Продължителност на _работата" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Продължителността на сесията за работа" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Продължителност на _почивката" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Продължителността на почивката между сесиите" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Стартиране след отключване" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Дали да започне хронометъра при отключване на екрана" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Няма документи за показване" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Билети" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Отваряне" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Избор на папка" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Настройки на „Кутия за билети“" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Местоположения" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Настройки на папката" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Къде Phosh търси за вашите билети" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Папка за билети" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Днес" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Утре" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Няма събития" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Цял ден" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Приключва на" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Неозаглавено събитие" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Няма събития за следващите %d дни" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Няма предстоящи събития" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Настройки за предстоящи събития" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Дни" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Период от време" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Брой дни, за които да се показват събития" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Мащаби на екраните" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Точки за достъп по Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Включване" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Тчк. за достъп е вкл." + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Тчк. за достъп е изкл." + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "След %u ден" +#~ msgstr[1] "След %u дена" diff --git a/po/ca.po b/po/ca.po new file mode 100644 index 000000000..8685b6ed8 --- /dev/null +++ b/po/ca.po @@ -0,0 +1,1326 @@ +# Walter Garcia-Fontes , 2018. #zanata +# Roberto MF , 2019. #zanata +# Marc Riera , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-12-21 09:46+0000\n" +"PO-Revision-Date: 2025-12-23 21:54+0100\n" +"Last-Translator: Marc Riera \n" +"Language-Team: Catalan\n" +"Language: ca\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Shell del telèfon" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Gestió de finestres i inici d'aplicacions per a mòbils" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Aquesta sessió us fa entrar al Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Configuració ràpida de mode cafeïna" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Impedeix que la sessió es quedi inactiva" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Calendari" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Un giny senzill de calendari" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Configuració ràpida de l'esquema de colors/mode fosc" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Commuta el mode fosc" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Informació d'emergència" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Mostra la informació i contactes d'emergència" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Capsa de llançadors" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Afegeix llançadors a la pantalla bloquejada. Aquest connector és " +"experimental." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Configuració ràpida d'ubicació" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Commuta els serveis d'ubicació" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Reproductors multimèdia" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Controleu els reproductors multimèdia en execució" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Configuració ràpida de dades mòbils" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Commuta l'ús de dades mòbils" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Configuració ràpida de la llum nocturna" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Commuta la llum nocturna" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Configuració ràpida del Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Simple temporitzador Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Caixa de tiquets" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Mostra fitxers PDF amb la pantalla bloquejada. Aquest connector és " +"experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Propers esdeveniments" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Mostra els propers esdeveniments del calendari" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Afegeix a la carpeta" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Crea una carpeta nova" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplicació" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Mostra totes les aplicacions" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostra només les aplicacions compatibles amb mòbils" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Bateria %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Activat" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "No s'ha trobat cap dispositiu Bluetooth connectable" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "El Bluetooth està desactivat" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Trucada desconeguda" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "La xarxa sense fil «%s» usa un portal captiu" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "La xarxa sense fil usa un portal captiu" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Connecta't a la xarxa sense fil" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Acoblat" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desacoblat" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "D'acord" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "No es pot establir una trucada d'emergència" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Error intern" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Surt" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "Es tancarà la sessió de %s automàticament d'aquí a %d segon." +msgstr[1] "Es tancarà la sessió de %s automàticament d'aquí a %d segons." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Apaga" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "El sistema s'apagarà automàticament d'aquí a %d segon." +msgstr[1] "El sistema s'apagarà automàticament d'aquí a %d segons." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Reinicia" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "El sistema es reiniciarà automàticament d'aquí a %d segon." +msgstr[1] "El sistema es reiniciarà automàticament d'aquí a %d segons." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Silenciós" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Desactivat" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Activat" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Voleu permetre que «%s» accedeixi a la informació de la ubicació?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Geolocalització" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Sí" + +#: src/location-manager.c:272 +msgid "No" +msgstr "No" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Introduïu la contrasenya" + +#: src/lockscreen.c:1040 +msgid "Checking…" +msgstr "S'està comprovant…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "S'ha desat la captura de pantalla a «%s»" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "No s'ha pogut desar la captura de pantalla" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Captura de pantalla" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Captures de pantalla" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Captura de pantalla de %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Títol desconegut" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Artista desconegut" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Pantalla interna" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Desconegut" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "No s'admet el tipus d'autenticació de la xarxa sense fil «%s»" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Introduïu la contrasenya de la xarxa sense fil «%s»" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Obre" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Notificació" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "ara" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%ddia" +msgstr[1] "%ddies" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 mes" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmes" +msgstr[1] "%dmes" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dany" +msgstr[1] "~%danys" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Més d'%d any" +msgstr[1] "Més de %d anys" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Gairebé %d any" +msgstr[1] "Gairebé %d anys" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "L'usuari ha descartat el diàleg d'autenticació" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Contrasenya:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "S'ha produït un error. Torneu-ho a provar." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Vertical" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Apaïsat" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desactivada" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Activada" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Premeu Esc per a tancar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "No s'ha pogut executar «%s»" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Control de volum del Phone Shell" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Les contrasenyes no coincideixen." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "La contrasenya no pot estar en blanc" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Llanterna" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Recorda la decisió" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Cancel·la" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Suprimeix dels _preferits" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Afegeix als _preferits" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Veieu els _detalls" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Desinstal·la" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Suprimeix de la carpeta" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Cerca aplicacions…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Dispositius de sortida" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Dispositius d'entrada" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Paràmetres de so" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Brillantor automàtica" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Paràmetres de la brillantor" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Activa el Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Paràmetres del Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Tanca la pantalla de trucada d'emergència" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Contactes d'emergència" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Torna a la pàgina de contactes d'emergència" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Torna a la pàgina del teclat d'emergència" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Propietari desconegut" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Contactes d'emergència" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "No hi ha disponible cap contacte d'emergència." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Algunes aplicacions estan ocupades o tenen feina sense desar" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Comentaris" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "No molesteu" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Paràmetres dels comentaris" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Usuari:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domini:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Co_nnecta" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Torna" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Llisqueu cap amunt per desbloquejar" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Desbloqueja" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Cal autenticació" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Cancel·la" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "C_onnecta" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Autenticació" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Suspèn" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Bloca" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Emergència" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Executa una ordre" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Cap notificació" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Notificacions" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Neteja-ho tot" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirmació:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "A_paga…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "Reinicia…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspèn…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Surt…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Sense fil" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Paràmetres de la xarxa sense fil" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "No s'ha trobat el connector" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "No s'ha pogut carregar el connector «%s» ." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "No s'ha trobat cap dispositiu de xarxa sense fil" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "La xarxa sense fil està desactivada" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Habilita la xarxa sense fil" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Punt d'accés de xarxa sense fil actiu" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Apaga" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "No hi ha cap punt d'accés Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mòbil" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "El Phosh està cafeïnat" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Desactivat" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Activat" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Temporitzadors de cafeïna" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "Sense intervals de cafeïna" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Sense temps límit" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Configuració ràpida de mode cafeïna" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Duració de la cafeïna" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Gestiona la duració de la cafeïna" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Afegiu o elimineu un interval de cafeïna personalitzat" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Afegeix un interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Afegeix un nou interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Afegeix" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Intervals ràpids" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Tria un interval" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Estil predeterminat" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Mode fosc" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Mode clar" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informació personal" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Data de naixement" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Idioma preferit" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Adreça particular" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informació mèdica" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Edat" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Grup sanguini" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Alçada" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Pes" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Al·lèrgies" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medicacions i condicions" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Altra informació" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferències de la informació d'emergència" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Fet" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Nom del pr_opietari" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Data de naixement" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Idioma _preferit" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Ed_at" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Grup sanguini" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Alçada" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Pes" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medicacions i condicions" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Afegeix el contacte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Afegeix un nou contacte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Nom de contacte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relació" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Número del _contacte" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "No hi ha cap llançador configurat" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Llançadors" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Ubicació activada" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Ubicació desactivada" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "No hi ha cap reproductor multimèdia en execució" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Dades mòbils activades" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Dades mòbils desactivades" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "La llum nocturna està activada" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "La llum nocturna està desactivada" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Inicia el Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Focalitzat en la tasca durant %d minuts" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Fes un descans" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Tens %d minuts fins al següent Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Temporitzador Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Atura del Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Configuració ràpida de les preferències del Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Tècnica Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Durada _activa" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Durada de la sessió de focalització" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Durada del _descans" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Durada del descans entre sessions" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Inicia en desbloquejar" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Si s'ha d'iniciar el temporitzador en desbloquejar la pantalla" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "No hi ha cap document per mostrar" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Tiquets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Obre" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Tria una carpeta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferències de la caixa de tiquets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Camins" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Configuració de la carpeta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Allà on Phosh cerca els vostres tiquets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Carpeta de tiquets" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Avui" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Demà" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Cap esdeveniment" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tot el dia" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Acaba" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Esdeveniment sense nom" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "No hi ha esdeveniments per als següents %d dies" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "No hi ha esdeveniments propers" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferències dels esdeveniments propers" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dies" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Rang de dates" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Nombre de dies per a mostrar-ne els esdeveniments" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Escalat del monitor" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Punt d'accés Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Encén" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Activa el punt d'accés Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Desactiva el punt d'accés Wi-Fi" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "En %u dia" +#~ msgstr[1] "En %u dies" + +#~ msgid "Add" +#~ msgstr "Afegeix" + +#~ msgid "Number" +#~ msgstr "Número" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "S'ha copiat la captura de pantalla al porta-retalls" + +#~ msgid "Scan" +#~ msgstr "Escaneja" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "dia" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "dies" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mes" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mesos" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "any" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "anys" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s %d %s" + +#~ msgid "App" +#~ msgstr "Aplicació" + +#~ msgid "_Power Off" +#~ msgstr "A_paga" + +#~ msgid "_Screenshot" +#~ msgstr "_Captura de pantalla" + +#~ msgid "Unknown application" +#~ msgstr "Aplicació desconeguda" + +#~ msgid "Lock Screen" +#~ msgstr "Bloqueja la pantalla" + +#~ msgid "Logout" +#~ msgstr "Surt" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Mostra només les aplicacions adaptatives" + +#~ msgid "Unknown artist" +#~ msgstr "Artista desconegut" + +#~ msgid "Unknown Song" +#~ msgstr "Cançó desconeguda" diff --git a/po/cs.po b/po/cs.po new file mode 100644 index 000000000..bc91a175a --- /dev/null +++ b/po/cs.po @@ -0,0 +1,1251 @@ +# Daniel Rusek , 2019. #zanata +# Jaroslav Svoboda , 2019-2021. +# Vojtěch Vengrin , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-10-25 14:25+0000\n" +"PO-Revision-Date: 2025-10-26 01:50+0200\n" +"Last-Translator: Daniel Rusek \n" +"Language-Team: Czech \n" +"Language: cs\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;\n" +"X-Generator: Poedit 3.7\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Shell telefonu" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Správa oken a spouštění aplikací pro mobilní zařízení" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Toto sezení vás přihlásí do prostředí Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Rychlé nastavení Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Zabránit nečinnosti sezení" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalendář" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Jednoduchý widget kalendáře" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Rychlé nastavení tmavého režimu / barevného schématu" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Přepnout tmavý režim" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Tísňové informace" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Zobrazit tísňové informace a kontakty" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Spouštěcí box" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Přidat spouštěče na zamykací obrazovku. Tento zásuvný modul je " +"experimentální." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Rychlé nastavení polohy" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Zapnutí/vypnutí služeb určování polohy" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Přehrávače médií" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Sledovat aktuálně spuštěné přehrávače médií" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Rychlé nastavení mobilních dat" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Zapnutí/vypnutí mobilních dat" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Rychlé nastavení nočního osvětlení" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Zapnutí/vypnutí nočního osvětlení" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Rychlé nastavení Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Jednoduchý časovač Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Rámeček lístků" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Zobrazit soubory PDF na zamykací obrazovce. Tento zásuvný modul je " +"experimentální." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Nadcházející události" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Zobrazit nadcházející události kalendáře" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Přidat do složky" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Vytvořit novou složku" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplikace" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Zobrazit všechny aplikace" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Zobrazit pouze optimalizované aplikace" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Baterie %.0f %%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Zapnout" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Nebyla nalezena žádná připojitelná zařízení Bluetooth" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth zakázáno" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Neznámý volající" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Síť Wi-Fi „%s“ využívá captive portál" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Síť Wi-Fi využívá captive portál" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Přihlásit se k síti Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "V doku" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Mobilní" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Nelze uskutečnit tísňové volání" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Vnitřní chyba" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Odhlásit se" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "Uživatel %s bude automaticky odhlášen za %d sekundu." +msgstr[1] "Uživatel %s bude automaticky odhlášen za %d sekundy." +msgstr[2] "Uživatel %s bude automaticky odhlášen za %d sekund." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Vypnout" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Systém bude automaticky vypnut za %d sekundu." +msgstr[1] "Systém bude automaticky vypnut za %d sekundy." +msgstr[2] "Systém bude automaticky vypnut za %d sekund." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Restartovat" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Systém bude automaticky restartován za %d sekundu." +msgstr[1] "Systém bude automaticky restartován za %d sekundy." +msgstr[2] "Systém bude automaticky restartován za %d sekund." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Ztlumeno" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Potichu" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Zapnout" + +#: src/location-manager.c:269 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Povolit aplikaci „%s“ přístup k vaší poloze?" + +#: src/location-manager.c:274 +msgid "Geolocation" +msgstr "Geolokace" + +#: src/location-manager.c:275 +msgid "Yes" +msgstr "Ano" + +#: src/location-manager.c:275 +msgid "No" +msgstr "Ne" + +#. give visual feedback on error +#: src/lockscreen.c:313 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Zadejte heslo" + +#: src/lockscreen.c:956 +msgid "Checking…" +msgstr "Kontroluje se…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Snímek obrazovky byl uložen do „%s“" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Nezdařilo se uložit snímek obrazovky" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Snímek obrazovky" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Snímky obrazovky" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Snímek obrazovky z %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:646 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Neznámý název" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:654 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Neznámý umělec" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Vestavěný displej" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Neznámé" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Typ autentizace sítě Wi-Fi „%s“ není podporován" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Vložte heslo sítě Wi-Fi „%s“" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Otevřít" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1012 +msgid "Notification" +msgstr "Upozornění" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "nyní" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30 sek." + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 min." + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 min." + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d min." +msgstr[1] "%d min." +msgstr[2] "%d min." + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d hod." +msgstr[1] "~%d hod." +msgstr[2] "~%d hod." + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 den" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d den" +msgstr[1] "%d dny" +msgstr[2] "%d dnů" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 měs." + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d měs." +msgstr[1] "%d měs." +msgstr[2] "%d měs." + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d rok" +msgstr[1] "~%d roky" +msgstr[2] "~%d let" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Více než %d rok" +msgstr[1] "Více než %d roky" +msgstr[2] "Více než %d let" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Téměř %d rok" +msgstr[1] "Téměř %d roky" +msgstr[2] "Téměř %d let" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Dialog ověření byl uživatelem zrušen" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Heslo:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Promiňte, toto nefungovalo. Zkuste to prosím znovu." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Na výšku" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Na šířku" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Vypnout" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Zapnout" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Stisknutím Escape zavřete" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Spuštění „%s“ selhalo" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Ovládání hlasitosti shellu telefonu" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Hesla nesouhlasí." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Heslo nemůže být prázdné" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Svítilna" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Zapamatovat si rozhodnutí" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Zrušit" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Odebrat z _oblíbených" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Přidat mezi _oblíbené" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Zobrazit po_drobnosti" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Odinstalovat" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Odeb_rat ze složky" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Hledat aplikace…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Výstupní zařízení" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Vstupní zařízení" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Nastavení zvuku" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Povolit Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Nastavení Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Zavřít dialog tísňového volání" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Tísňové _kontakty" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Přejít na stránku tísňových kontaktů" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Vrátit se na stránku tísňového vytáčení" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Neznámý vlastník" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Tísňové kontakty" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Nejsou k dispozici žádné tísňové kontakty." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Některé aplikace jsou zaneprázdněné nebo obsahují neuloženou práci" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Zpětná vazba" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Nerušit" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Nastavení zpětné vazby" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Uživatel:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Doména:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Připojit" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Zpět" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Táhnutím nahoru odemknete" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Odemknout" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Požadováno ověření" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Zrušit" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Připojit" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Ověřit" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Uspat" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Uzamknout" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Stav nouze" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Spustit příkaz" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Žádná upozornění" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Upozornění" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Vymazat vše" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Potvrdit:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "Vy_pnout…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Restartovat…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "U_spat…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Odh_lásit se…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +msgid "Wi-Fi Settings" +msgstr "Nastavení WiFi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e. %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Zásuvný modul nebyl nalezen" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Zásuvný modul „%s“ nelze načíst." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Nebylo nalezeno žádné zařízení Wi-Fi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi zakázáno" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Povolit Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Přístupový bod Wi-Fi je aktivní" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Vypnout" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Žádné přístupové body Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobilní síť" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:50 +msgid "Phosh on caffeine" +msgstr "Phosh na caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:129 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Zapnout" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:129 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Vypnout" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Výchozí styl" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Tmavý režim" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Světlý režim" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Osobní informace" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Datum narození" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Preferovaný jazyk" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Adresa bydliště" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Zdravotní informace" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Věk" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Krevní skupina" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Výška" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Váha" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergie" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Léky a zdravotní stav" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Další informace" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Předvolby nouzových informací" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Hotovo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Jméno vlastníka" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Datum narození" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Preferovaný jazyk" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Věk" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "Krevní _skupina" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "Výš_ka" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "Vá_ha" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Léky a zdravotní stav" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Přidat kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Přidat nový kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "Přid_at" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Jméno _kontaktu" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Vztah" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Číslo _kontaktu" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Nejsou nakonfigurovány žádné spouštěče" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Spouštěče" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Poloha zapnuta" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Poloha vypnuta" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Žádné spuštěné přehrávače médií" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Mobilní data zapnuta" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Mobilní data vypnuta" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Noční osvětlení zapnuto" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Noční osvětlení vypnuto" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodoro zahájeno" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Soustřeďte se na svůj úkol po dobu %d minut" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Dejte si pauzu" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Do příštího Pomodoro vám zbývá %d minut" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Časovač Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro vypnuto" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Předvolby rychlého nastavení Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Technika Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Trvání _aktivity" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Doba trvání soustředění" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Trvání _pauzy" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Doba trvání přestávky mezi sezeními" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Spustit po odemknutí" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Zda spustit časovač po odemknutí obrazovky" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Žádné dokumenty k zobrazení" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Lístky" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Otevřít" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Vyberte složku" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Předvolby rámečku lístků" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Cesty" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Nastavení složky" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Kde Phosh hledá vaše lístky" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Složka lístků" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Dnes" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Zítra" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x, %a." + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Žádné události" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l∶%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Celý den" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Končí" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Bezejmenná událost" + +#: plugins/upcoming-events/upcoming-events.c:372 +#, c-format +msgid "No events for the next %d days" +msgstr "Žádné události na příštích %d dní" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Žádné nadcházející události" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Předvolby nadcházejících událostí" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dny" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Časové období" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Počet dní pro zobrazení událostí" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Škálování monitoru" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d %%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:75 +msgid "Hotspot On" +msgstr "Přístupový bod zapnut" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:77 +msgid "Hotspot Off" +msgstr "Přístupový bod vypnut" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "Za %u den" +#~ msgstr[1] "Za %u dny" +#~ msgstr[2] "Za %u dní" + +#~ msgid "Add" +#~ msgstr "Přidat" + +#~ msgid "Number" +#~ msgstr "Číslo" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Snímek obrazovky byl zkopírován do schránky" + +#~ msgid "Scan" +#~ msgstr "Skenovat" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "měs" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "měs" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "r" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "r" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplikace" + +#~ msgid "_Power Off" +#~ msgstr "Vy_pnout" + +#~ msgid "_Screenshot" +#~ msgstr "_Snímek obrazovky" + +#~ msgid "Unknown application" +#~ msgstr "Neznámá aplikace" + +#~ msgid "Lock Screen" +#~ msgstr "Zamknout obrazovku" + +#~ msgid "Logout" +#~ msgstr "Odhlásit" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d. %m. %y" diff --git a/po/da.po b/po/da.po new file mode 100644 index 000000000..79d763e5e --- /dev/null +++ b/po/da.po @@ -0,0 +1,405 @@ +# Danish translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Simon Jespersen , 2018-2019. #zanata +# scootergrisen, 2020-2021. +# scootergrisen: oversættelsen mangler at blive testet +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/phosh/issues\n" +"POT-Creation-Date: 2021-04-01 03:31+0000\n" +"PO-Revision-Date: 2021-04-15 00:00+0200\n" +"Last-Translator: scootergrisen\n" +"Language-Team: Danish\n" +"Language: da\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Telefonskal" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Vindueshåndtering og programstarter til mobiltelefon" + +#: src/app-grid-button.c:536 +msgid "Application" +msgstr "Program" + +#: src/bt-info.c:92 src/feedbackinfo.c:51 src/rotateinfo.c:103 +msgid "On" +msgstr "Til" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Forankeret" + +#: src/docked-info.c:81 src/docked-info.c:195 +msgid "Undocked" +msgstr "Ikke forankeret" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Log ud" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s logges automatisk ud om %d sekund." +msgstr[1] "%s logges automatisk ud om %d sekunder." + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:37 +msgid "Power Off" +msgstr "Sluk" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Systemet slukkes automatisk om %d sekund." +msgstr[1] "Systemet slukkes automatisk om %d sekunder." + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:30 +msgid "Restart" +msgstr "Genstart" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Systemet genstartes automatisk om %d sekund." +msgstr[1] "Systemet genstartes automatisk om %d sekunder." + +#: src/end-session-dialog.c:270 +#| msgid "Application" +msgid "Unknown application" +msgstr "Ukendt program" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:44 +msgid "Quiet" +msgstr "Stille" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:49 +msgid "Silent" +msgstr "Lydløs" + +#: src/location-manager.c:246 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Tillad at '%s' får adgang til din placeringsinformation?" + +#: src/location-manager.c:251 +#| msgid "Application" +msgid "Geolocation" +msgstr "Geoplacering" + +#: src/location-manager.c:252 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:252 +msgid "No" +msgstr "Nej" + +#: src/lockscreen.c:85 src/ui/lockscreen.ui:234 +msgid "Enter Passcode" +msgstr "Indtast adgangskode" + +#: src/lockscreen.c:264 +msgid "Checking…" +msgstr "Tjekker …" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:342 +msgid "%A, %B %-e" +msgstr "%A %d. %B" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:277 src/ui/media-player.ui:107 +msgid "Unknown Title" +msgstr "Ukendt titel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:286 src/ui/media-player.ui:127 +msgid "Unknown Artist" +msgstr "Ukendt kunstner" + +#: src/monitor-manager.c:112 +msgid "Built-in display" +msgstr "Indbygget skærm" + +#: src/monitor-manager.c:130 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %sn" +msgstr "%s %sn" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:146 +msgid "Unknown" +msgstr "Ukendt" + +#: src/network-auth-prompt.c:187 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Godkendelsestypen for wifi-netværket “%s” understøttes ikke" + +#: src/network-auth-prompt.c:192 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Indtast adgangskode til wifi-netværket “%s”" + +#: src/notifications/mount-notification.c:137 +msgid "Open" +msgstr "Åbn" + +#: src/notifications/notification.c:381 src/notifications/notification.c:637 +msgid "Notification" +msgstr "Underretning" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "t" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "t" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "må" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "må" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "å" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "å" + +#: src/notifications/timestamp-label.c:121 +#| msgid "Unknown" +msgid "now" +msgstr "nu" + +# scootergrisen: tjek at det er korrekt +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +#| msgid "Over" +msgid "Over %dy" +msgstr "Over %då" + +# scootergrisen: tjek at det er korrekt +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +#| msgid "Almost" +msgid "Almost %dy" +msgstr "Næsten %då" + +# scootergrisen: tjek at det er korrekt +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:225 +msgid "Authentication dialog was dismissed by the user" +msgstr "Godkendelsesdialogen blev lukket af brugeren" + +#: src/polkit-auth-prompt.c:278 src/ui/network-auth-prompt.ui:85 +#: src/ui/polkit-auth-prompt.ui:57 src/ui/system-prompt.ui:33 +msgid "Password:" +msgstr "Adgangskode:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Beklager, det virkede ikke. Prøv venligst igen." + +#: src/rotateinfo.c:81 +msgid "Portrait" +msgstr "Portræt" + +#: src/rotateinfo.c:84 +msgid "Landscape" +msgstr "Landskab" + +#. Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) +#. Translators: Automatic screen orientation is off (locked/disabled) +#: src/rotateinfo.c:103 src/rotateinfo.c:183 +msgid "Off" +msgstr "Fra" + +#: src/system-prompt.c:364 +msgid "Passwords do not match." +msgstr "Adgangskoderne er ikke ens." + +#: src/system-prompt.c:371 +msgid "Password cannot be blank" +msgstr "Adgangskoden må ikke være tom" + +# scootergrisen: ved ikke hvad "Torch" er. Tjek at det er korrekt oversat +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lommelygte (Torch)" + +#: src/ui/app-auth-prompt.ui:41 +msgid "Remember decision" +msgstr "Husk beslutning" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:50 +#| msgid "_Cancel" +msgid "Cancel" +msgstr "Annuller" + +#: src/ui/app-auth-prompt.ui:66 src/ui/end-session-dialog.ui:61 +msgid "Ok" +msgstr "OK" + +#: src/ui/app-grid-button.ui:49 +msgid "App" +msgstr "Program" + +#: src/ui/app-grid-button.ui:76 +msgid "Remove from _Favorites" +msgstr "Fjern fra _favoritter" + +#: src/ui/app-grid-button.ui:81 +msgid "Add to _Favorites" +msgstr "Tilføj til _favoritter" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Søger efter programmer …" + +#: src/ui/end-session-dialog.ui:32 +msgid "Some applications are busy or have unsaved work" +msgstr "Nogle programmer er optagede eller har arbejde som ikke er blevet gemt" + +#: src/ui/lockscreen.ui:37 +msgid "Slide up to unlock" +msgstr "Skub op for at låse op" + +#: src/ui/lockscreen.ui:280 +msgid "Emergency" +msgstr "Nødopkald" + +#: src/ui/lockscreen.ui:296 +msgid "Unlock" +msgstr "Lås op" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:7 +#| msgid "Authenticate" +msgid "Authentication required" +msgstr "Godkendelse kræves" + +#: src/ui/network-auth-prompt.ui:42 +msgid "_Cancel" +msgstr "_Annuller" + +#: src/ui/network-auth-prompt.ui:61 +msgid "C_onnect" +msgstr "_Opret forbindelse" + +#: src/ui/polkit-auth-prompt.ui:125 +msgid "Authenticate" +msgstr "Godkend" + +#: src/ui/system-prompt.ui:63 +msgid "Confirm:" +msgstr "Bekræft:" + +#: src/ui/top-panel.ui:16 +msgid "Lock Screen" +msgstr "Lås skærm" + +#: src/ui/top-panel.ui:23 +msgid "Logout" +msgstr "Log ud" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:172 +msgid "Cellular" +msgstr "Mobil" diff --git a/po/de.po b/po/de.po new file mode 100644 index 000000000..aee335166 --- /dev/null +++ b/po/de.po @@ -0,0 +1,1343 @@ +# German translation for phosh +# Copyright (C) 2018 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# +# Guido Günther , 2018 +# Daniel Brunkhorst , 2018. #zanata +# Guido Günther , 2018. #zanata +# kai , 2018. #zanata +# Guido Günther , 2019. #zanata +# John Smith , 2019. #zanata +# M.Ehl <1mail4me@web.de>, 2019. #zanata +# Max Schmidt , 2019. #zanata +# Mike Ballmann , 2019. #zanata +# Mike Ballmann , 2020. #zanata +# heiko123abc , 2020. +# Tim Sabsch , 2020, 2024. +# Michael Oppliger , 2021. +# Philipp Kiemle , 2021, 2023-2025. +# Jürgen Benvenuti , 2022-2026. +# +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-12-31 16:32+0000\n" +"PO-Revision-Date: 2026-01-08 21:56+0100\n" +"Last-Translator: Jürgen Benvenuti \n" +"Language-Team: German \n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Telefon-Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Fensterverwaltung und Starten von Anwendungen für Mobilgeräte" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Diese Sitzung meldet Sie in Phosh an" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Koffein-Schnelleinstellungen" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Inaktivwerden der Sitzung verhindern" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalender" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Ein einfaches Kalender-Widget" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Dunkler Modus/Farbschema-Schnelleinstellungen" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Dunklen Modus ein-/ausschalten" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Notfallinformationen" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Notfallinformationen und -kontakte anzeigen" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Starterbox" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Starter zum Sperrbildschirm hinzufügen. Diese Erweiterung ist " +"experimentell." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Standort-Schnelleinstellungen" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Ortungsdienste ein-/ausschalten" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Medienspieler" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Aktuell laufende Medienspieler verfolgen" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Mobile Daten-Schnelleinstellungen" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Mobile Daten ein-/ausschalten" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Nachtmodus-Schnelleinstellungen" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Nachtmodus ein-/ausschalten" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro-Schnelleinstellungen" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Einfacher Pomodoro-Zeitmesser" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Ticketbox" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"PDFs auf dem Sperrbildschirm anzeigen. Diese Erweiterung ist " +"experimentell." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Anstehende Termine" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Anstehende Kalendertermine anzeigen" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Zum Ordner hinzufügen" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Neuen Ordner erstellen" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Anwendung" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Alle Anwendungen anzeigen" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Nur mobiloptimierte Anwendungen anzeigen" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Akku %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "An" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Keine verbindbaren Bluetooth-Geräte gefunden" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth deaktiviert" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Unbekannter Anrufer" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Das WLAN-Netzwerk »%s« verwendet ein Captive Portal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Das WLAN-Netzwerk verwendet ein Captive Portal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Im WLAN-Netzwerk anmelden" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Angedockt" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Abgedockt" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Absetzen eines Notrufs nicht möglich" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Interner Fehler" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Abmelden" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s wird automatisch in %d Sekunde abgemeldet." +msgstr[1] "%s wird automatisch in %d Sekunden abgemeldet." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Ausschalten" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Das System wird in %d Sekunde ausgeschaltet." +msgstr[1] "Das System wird in %d Sekunden ausgeschaltet." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Neustart" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Das System wird in %d Sekunde neu gestartet." +msgstr[1] "Das System wird in %d Sekunden neu gestartet." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Leise" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Lautlos" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "An" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Zulassen, dass »%s« Ihre Standortinformationen abruft?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Standort" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Nein" + +#. give visual feedback on error +#: src/lockscreen.c:397 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Zugangscode eingeben" + +#: src/lockscreen.c:1041 +msgid "Checking…" +msgstr "Überprüfung läuft …" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Bildschirmfoto wurde in »%s« gespeichert" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Bildschirmfoto konnte nicht gespeichert werden" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Bildschirmfoto" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Bildschirmfotos" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Bildschirmfoto vom %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Unbekannter Titel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Unbekannter Interpret" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Eingebaute Anzeige" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Unbekannt" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "" +"Die Art der Legitimierung des WLAN-Netzwerks »%s« wird nicht unterstützt" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Geben Sie das Passwort für das WLAN-Netzwerk »%s« ein" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Öffnen" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Benachrichtigung" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "Jetzt" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "< 30 s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "< 1 min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~ 1 min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d min" +msgstr[1] "%d min" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d H" +msgstr[1] "~%d H" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~ 1 T" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d T" +msgstr[1] "%d T" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~ 1 M" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d M" +msgstr[1] "%d M" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d J" +msgstr[1] "~%d J" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Über %d J" +msgstr[1] "Über %d J" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Fast %d J" +msgstr[1] "Fast %d J" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Der Dialog zur Anmeldung wurde vom Benutzer geschlossen" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Passwort:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "" +"Entschuldigung, das hat nicht funktioniert. Bitte versuchen Sie es erneut." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Hochformat" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Querformat" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Aus" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "An" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Drücken Sie Esc zum Schließen" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "»%s« konnte nicht ausgeführt werden" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Telefon-Shell-Lautstärkeregelung" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Die Passwörter stimmen nicht überein." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Das Passwort darf nicht leer sein" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Taschenlampe" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Entscheidung merken" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Abbrechen" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Aus _Favoriten entfernen" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Zu _Favoriten hinzufügen" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "_Details betrachten" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Deinstallieren" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Aus dem _Ordner entfernen" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Anwendungen suchen …" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Ausgabegeräte" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Eingabegeräte" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Klangeinstellungen" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Automatische Helligkeit" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Helligkeitseinstellungen" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Bluetooth aktivieren" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth-Einstellungen" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Den Notrufdialog schließen" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Notfall_kontakte" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Zur Notfallkontakte-Seite wechseln" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Zur Notfallziffernblock-Seite zurückgehen" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Besitzer unbekannt" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Notfallkontakte" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Keine Notfallkontakte verfügbar." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Einige Anwendungen sind beschäftigt oder haben ungesicherte Änderungen" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Feedback" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Nicht stören" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Feedback-Einstellungen" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Benutzer:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domäne:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Verbinden" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Zurück" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Nach oben wischen zum Entsperren" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Entsperren" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Legitimierung erforderlich" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Abbrechen" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Verbinden" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Legitimieren" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Bereitschaft" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Sperren" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Notfall" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Befehl ausführen" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Keine Benachrichtigungen" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Benachrichtigungen" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Alles leeren" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Bestätigen:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Ausschalten …" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Neu starten …" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Bereitschaft …" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Abmelden …" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "WLAN" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "WLAN-Einstellungen" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A %d. %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Erweiterung nicht gefunden" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Die Erweiterung »%s« konnte nicht geladen werden." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Kein WLAN-Gerät gefunden" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "WLAN deaktiviert" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "WLAN aktivieren" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "WLAN-Hotspot aktiv" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Ausschalten" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Keine WLAN-Hotspots" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobilfunk" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh auf Koffein" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Aus" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "An" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Koffein-Timer" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "Keine Koffein-Intervalle" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Keine Zeitüberschreitung (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Koffein-Schnelleinstellungen" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Koffein-Dauer" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Koffein-Dauer verwalten" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Benutzerdefinierte Koffein-Intervalle hinzufügen oder entfernen" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Intervall hinzufügen" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Neues Intervall hinzufügen" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Hinzufügen" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Schnellstart-Intervalle" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Intervall auswählen" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Standardstil" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Dunkler Modus" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Heller Modus" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Persönliche Informationen" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Geburtsdatum" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Bevorzugte Sprache" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Privatadresse" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Medizinische Informationen" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Alter" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Blutgruppe" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Größe" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Gewicht" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergien" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medikamente und Erkrankungen" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Weitere Informationen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Einstellungen für Notfallinformationen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Fertig" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Name des _Besitzers" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Geburts_datum" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Bevorzugte S_prache" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Alter" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Blutgruppe" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Höhe" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "Ge_wicht" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medikamente und Erkrankungen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Kontakt hinzufügen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Neuen Kontakt hinzufügen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Kontaktname" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Beziehung" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Kontaktnummer" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Keine Starter eingerichtet" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Starter" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Standort ein" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Standort aus" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Keine laufenden Medienspieler" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Mobile Daten an" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Mobile Daten aus" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Nachtmodus an" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Nachtmodus aus" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodoro Start" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Konzentrieren Sie sich für %d Minuten auf Ihre Aufgabe" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Legen Sie eine Pause ein" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Sie haben %d Minuten bis zum nächsten Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Pomodoro-Zeitmesser" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Aus" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro-Schnelleinstellung-Einstellungen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro-Technik" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Aktive Dauer" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Dauer der Fokussitzung" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Pausendauer" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Dauer der Pause zwischen den Sitzungen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "Beim Entsperren _starten" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Ob beim Entsperren des Bildschirms der Timer gestartet werden soll" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Keine anzuzeigenden Dokumente" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Öffnen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Ordner auswählen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Ticketbox-Einstellungen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Pfade" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Ordnereinstellungen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Wo Phosh nach Ihren Tickets sucht" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Ticketordner" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Heute" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Morgen" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Keine Termine" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Ganztägig" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Endet" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Unbenannter Termin" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Kein Termin für die nächsten %d Tage" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Keine anstehenden Termine" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Einstellungen für anstehende Termine" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Tage" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Datumsbereich" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Anzahl der Tage, für die Termine angezeigt werden sollen" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Bildschirmskalierung" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d %%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "WLAN-Hotspot" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Einschalten" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Hotspot an" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Hotspot aus" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "In %u Tag" +#~ msgstr[1] "In %u Tagen" + +#~ msgid "Add" +#~ msgstr "Hinzufügen" + +#~ msgid "Number" +#~ msgstr "Nummer" + +# Hier geht es um WLAN-Netze laut .po-Datei (wifi-status-page.ui). - jb +#~ msgid "Scan" +#~ msgstr "Suchen" + +#~ msgid "_Power Off" +#~ msgstr "_Ausschalten" + +#~ msgid "_Screenshot" +#~ msgstr "Bildschirm_foto" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "T" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "T" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "Mon" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "Mon" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "J" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "J" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Anwendung" + +#~ msgid "Unknown application" +#~ msgstr "Unbekannte Anwendung" + +#~ msgid "Lock Screen" +#~ msgstr "Bildschirm sperren" + +#~ msgid "Logout" +#~ msgstr "Abmelden" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Nur adaptive Apps anzeigen" + +#~ msgid "Unknown artist" +#~ msgstr "Unbekannter Künstler" + +#~ msgid "Unknown Song" +#~ msgstr "Unbekannter Titel" diff --git a/po/el.po b/po/el.po new file mode 100644 index 000000000..757a1547b --- /dev/null +++ b/po/el.po @@ -0,0 +1,1306 @@ +# Pan Talianos , 2019. #zanata +# Ioannis , 2020. #zanata +# Efstathios Iosifidis , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-12-31 16:32+0000\n" +"PO-Revision-Date: 2023-08-20 00:55+0300\n" +"Last-Translator: Efstathios Iosifidis \n" +"Language-Team: Greek, Modern (1453-) \n" +"Language: el\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Κέλυφος Τηλεφώνου" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Διαχείριση παραθύρων και εκκίνηση εφαρμογών για κινητά" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Αυτή η συνεδρία σας συνδέει στο Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Γρήγορη Ρύθμιση Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Αποτροπή αδράνειας συνεδρίας" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Ημερολόγιο" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Ένα απλό widget ημερολογίου" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Γρήγορη Ρύθμιση Σκούρας Λειτουργίας / Χρωματικού Συνδυασμού" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Εναλλαγή σκούρας λειτουργίας" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Πληροφορίες Έκτακτης Ανάγκης" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Εμφάνιση πληροφοριών και επαφών έκτακτης ανάγκης" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Πλαίσιο Εκκινητών" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Προσθήκη εκκινητών στην οθόνη κλειδώματος. Αυτό το πρόσθετο είναι " +"πειραματικό." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Γρήγορη Ρύθμιση Τοποθεσίας" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Εναλλαγή υπηρεσιών τοποθεσίας ενεργό/ανενεργό" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Αναπαραγωγείς Πολυμέσων" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Παρακολούθηση τρεχόντων αναπαραγωγέων πολυμέσων" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Γρήγορη Ρύθμιση Κινητών Δεδομένων" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Εναλλαγή κινητών δεδομένων ενεργό/ανενεργό" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Γρήγορη Ρύθμιση Νυχτερινού Φωτός" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Εναλλαγή νυχτερινού φωτός ενεργό/ανενεργό" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Γρήγορη Ρύθμιση Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Απλός Χρονομετρητής Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Πλαίσιο Εισιτηρίων" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Εμφάνιση αρχείων PDF στην οθόνη κλειδώματος. Αυτό το πρόσθετο είναι " +"πειραματικό." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Επερχόμενα Συμβάντα" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Εμφάνιση επερχόμενων συμβάντων ημερολογίου" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Προσθήκη σε Φάκελο" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Δημιουργία νέου φακέλου" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Εφαρμογή" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Εμφάνιση Όλων των Εφαρμογών" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Εμφάνιση Μόνο Εφαρμογών Φιλικών προς Κινητά" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Μπαταρία %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Ενεργό" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Δεν βρέθηκαν συσκευές Bluetooth που μπορούν να συνδεθούν" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Το Bluetooth είναι απενεργοποιημένο" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Άγνωστος καλών" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Το δίκτυο Wi-Fi '%s' χρησιμοποιεί captive portal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Το δίκτυο Wi-Fi χρησιμοποιεί captive portal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Σύνδεση στο δίκτυο Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Σε Θήκη" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Εκτός Θήκης" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "ΟΚ" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Αδυναμία πραγματοποίησης κλήσης έκτακτης ανάγκης" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Εσωτερικό σφάλμα" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Αποσύνδεση" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "Ο %s θα αποσυνδεθεί αυτόματα σε %d δευτερόλεπτο." +msgstr[1] "Ο %s θα αποσυνδεθεί αυτόματα σε %d δευτερόλεπτα." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Απενεργοποίηση" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Το σύστημα θα απενεργοποιηθεί αυτόματα σε %d δευτερόλεπτο." +msgstr[1] "Το σύστημα θα απενεργοποιηθεί αυτόματα σε %d δευτερόλεπτα." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Επανεκκίνηση" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Το σύστημα θα επανεκκινηθεί αυτόματα σε %d δευτερόλεπτο." +msgstr[1] "Το σύστημα θα επανεκκινηθεί αυτόματα σε %d δευτερόλεπτα." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Σιωπηλό" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Αθόρυβο" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Ενεργό" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Να επιτραπεί στο '%s' η πρόσβαση στις πληροφορίες τοποθεσίας σας;" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Γεωτοποθεσία" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Ναι" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Όχι" + +#. give visual feedback on error +#: src/lockscreen.c:397 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Εισαγωγή Κωδικού Πρόσβασης" + +#: src/lockscreen.c:1041 +msgid "Checking…" +msgstr "Γίνεται έλεγχος…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Το στιγμιότυπο οθόνης αποθηκεύτηκε στο '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Αποτυχία αποθήκευσης στιγμιότυπου οθόνης" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Στιγμιότυπο οθόνης" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Στιγμιότυπα Οθόνης" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Στιγμιότυπο οθόνης από %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Άγνωστος Τίτλος" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Άγνωστος Καλλιτέχνης" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Ενσωματωμένη οθόνη" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Άγνωστο" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Ο τύπος πιστοποίησης του δικτύου Wi-Fi «%s» δεν υποστηρίζεται" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Εισαγωγή κωδικού πρόσβασης για το δίκτυο Wi-Fi «%s»" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Άνοιγμα" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Ειδοποίηση" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "τώρα" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30δ" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1λ" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1λ" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dλ" +msgstr[1] "%dλ" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dω" +msgstr[1] "~%dω" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1η" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dη" +msgstr[1] "%dη" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1μ" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dμ" +msgstr[1] "%dμ" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dχ" +msgstr[1] "~%dχ" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Πάνω από %dχ" +msgstr[1] "Πάνω από %dχ" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Σχεδόν %dχ" +msgstr[1] "Σχεδόν %dχ" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Ο διάλογος πιστοποίησης ακυρώθηκε από τον χρήστη" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Κωδικός πρόσβασης:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Συγγνώμη, αυτό δεν λειτούργησε. Παρακαλώ δοκιμάστε ξανά." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Πορτρέτο" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Τοπίο" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Ανενεργό" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Ενεργό" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Πατήστε ESC για κλείσιμο" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Η εκτέλεση του '%s' απέτυχε" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Έλεγχος Έντασης Κελύφους Τηλεφώνου" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Οι κωδικοί πρόσβασης δεν ταιριάζουν." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Ο κωδικός πρόσβασης δεν μπορεί να είναι κενός" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Φακός" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Απομνημόνευση απόφασης" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Ακύρωση" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Αφαίρεση από τα _Αγαπημένα" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Προσθήκη στα _Αγαπημένα" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Προβολή _Λεπτομερειών" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Απεγκατάσταση" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Αφαίρεση από Φάκελο" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Αναζήτηση εφαρμογών…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Συσκευές Εξόδου" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Συσκευές Εισόδου" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Ρυθμίσεις Ήχου" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Αυτόματη Φωτεινότητα" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Ρυθμίσεις Φωτεινότητας" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Ενεργοποίηση Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Ρυθμίσεις Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Κλείσιμο διαλόγου κλήσης έκτακτης ανάγκης" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Επαφές _Έκτακτης Ανάγκης" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Μετάβαση στη σελίδα επαφών έκτακτης ανάγκης" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Επιστροφή στη σελίδα πληκτρολογίου έκτακτης ανάγκης" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Άγνωστος κάτοχος" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Επαφές Έκτακτης Ανάγκης" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Δεν υπάρχουν διαθέσιμες επαφές έκτακτης ανάγκης." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "" +"Ορισμένες εφαρμογές είναι απασχολημένες ή έχουν μη αποθηκευμένη εργασία" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Ανατροφοδότηση" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Να μην ενοχλείτε" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Ρυθμίσεις Ανατροφοδότησης" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Χρήστης:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Τομέας:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Σύν_δεση" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Πίσω" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Σύρετε προς τα πάνω για ξεκλείδωμα" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Ξεκλείδωμα" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Απαιτείται πιστοποίηση" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Ακύρωση" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "Σύν_δεση" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Πιστοποίηση" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Αναστολή" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Κλείδωμα" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Επείγον" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Εκτέλεση Εντολής" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Δεν υπάρχουν ειδοποιήσεις" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Ειδοποιήσεις" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Καθαρισμός όλων" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Επιβεβαίωση:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Απενεργοποίηση…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Επανεκκίνηση…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Αναστολή…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Αποσύνδεση…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Ρυθμίσεις Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Το πρόσθετο δεν βρέθηκε" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Το πρόσθετο '%s' δεν μπόρεσε να φορτωθεί." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Δεν Βρέθηκε Συσκευή Wi-Fi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Το Wi-Fi είναι Απενεργοποιημένο" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Ενεργοποίηση Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Ενεργό Hotspot Wi-Fi" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Απενεργοποίηση" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Κανένα Hotspot Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Κινητό Δίκτυο" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh με caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Ανενεργό" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Ενεργό" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Χρονομετρητές caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "Κανένα διάστημα caffeine" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Χωρίς χρονικό όριο (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Προτιμήσεις Γρήγορης Ρύθμισης Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Διάρκεια Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Διαχείριση διάρκειας Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Προσθήκη ή αφαίρεση προσαρμοσμένων διαστημάτων Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Προσθήκη διαστήματος" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Προσθήκη νέου διαστήματος" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Προσθήκη" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Διαστήματα γρήγορης εκκίνησης" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 λ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 λ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 λ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 ω" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Επιλογή διαστήματος" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Προεπιλεγμένο στυλ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Σκούρα λειτουργία" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Φωτεινή λειτουργία" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Προσωπικές Πληροφορίες" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Ημερομηνία Γέννησης" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Προτιμώμενη Γλώσσα" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Διεύθυνση Κατοικίας" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Ιατρικές Πληροφορίες" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Ηλικία" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Ομάδα Αίματος" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Ύψος" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Βάρος" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Αλλεργίες" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Φάρμακα & Παθήσεις" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Άλλες Πληροφορίες" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Προτιμήσεις Πληροφοριών Έκτακτης Ανάγκης" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "ΟΚ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Όνομα Κατόχου" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Ημερομηνία Γέννησης" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Προτιμώμενη Γλώσσα" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Ηλικία" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Ομάδα Αίματος" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Ύψος" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Βάρος" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Φάρμακα και Παθήσεις" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Προσθήκη Επαφής" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Προσθήκη Νέας Επαφής" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Όνομα Επαφής" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Σχέση" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Αριθμός Επαφής" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Δεν έχουν ρυθμιστεί εκκινητές" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Εκκινητές" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Τοποθεσία Ενεργή" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Τοποθεσία Ανενεργή" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Κανένας ενεργός αναπαραγωγέας πολυμέσων" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Κινητά Δεδομένα Ενεργά" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Κινητά Δεδομένα Ανενεργά" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Νυχτερινό Φως Ενεργό" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Νυχτερινό Φως Ανενεργό" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Εκκίνηση Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Εστίαση στην εργασία σας για %d λεπτά" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Κάντε ένα διάλειμμα" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Έχετε %d λεπτά μέχρι το επόμενο Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Χρονομετρητής Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Ανενεργό" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Προτιμήσεις Γρήγορης Ρύθμισης Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Τεχνική Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Διάρκεια Ενεργότητας" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Διάρκεια συνεδρίας εστίασης" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Διάρκεια Διαλείμματος" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Διάρκεια του διαλείμματος μεταξύ συνεδριών" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Εκκίνηση κατά το ξεκλείδωμα" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Εάν θα ξεκινά ο χρονομετρητής κατά το ξεκλείδωμα οθόνης" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Δεν υπάρχουν έγγραφα προς εμφάνιση" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Εισιτήρια" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "Άνοι_γμα" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Επιλογή φακέλου" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Προτιμήσεις Πλαισίου Εισιτηρίων" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Μονοπάτια" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Ρυθμίσεις φακέλου" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Όπου ο Phosh αναζητά τα εισιτήριά σας" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Φάκελος Εισιτηρίων" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Σήμερα" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Αύριο" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Χωρίς συμβάντα" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Όλη μέρα" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Τελειώνει" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Άτιτλο συμβάν" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Κανένα συμβάν για τις επόμενες %d ημέρες" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Κανένα επερχόμενο συμβάν" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Προτιμήσεις Επερχόμενων Συμβάντων" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Ημέρες" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Εύρος Ημερομηνιών" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Αριθμός ημερών για εμφάνιση συμβάντων" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Κλίμακες οθόνης" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Hotspot Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Ενεργοποίηση" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Hotspot Ενεργό" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Hotspot Ανενεργό" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "δ" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "λ" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "λ" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "ω" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "ω" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "ημ" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "ημ" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "μην" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "μην" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "ετος" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "έτη" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Εφαρμογή" + +#~ msgid "_Screenshot" +#~ msgstr "_Στιγμιότυπο οθόνης" + +#~ msgid "Number" +#~ msgstr "Αριθμός" + +#, c-format +#~ msgid "In %d day" +#~ msgid_plural "In %d days" +#~ msgstr[0] "Σε %d ημέρα" +#~ msgstr[1] "Σε %d ημέρες" + +#~ msgid "Lock Screen" +#~ msgstr "Κλείδωμα οθόνης" + +#~ msgid "Logout" +#~ msgstr "Έξοδος" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Εμφάνιση μόνο προσαρμοσμένων εφαρμογών" diff --git a/po/en_GB.po b/po/en_GB.po new file mode 100644 index 000000000..d13be5388 --- /dev/null +++ b/po/en_GB.po @@ -0,0 +1,1195 @@ +# son of dirt , 2019. +# Zander Brown , 2020-2021. +# Andi Chandler , 2024-2025. +# Bruce Cowan , 2024-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-04-30 09:07+0000\n" +"PO-Revision-Date: 2025-04-22 16:07+0100\n" +"Last-Translator: Andi Chandler \n" +"Language-Team: English - United Kingdom \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Window management and application launching for mobile" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "This session logs you into Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Caffeine Quick Setting" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Prevent the session from going idle" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendar" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "A simple calendar widget" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Dark Mode / Colour Scheme Quick Setting" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Toggle dark mode" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Emergency Info" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Show emergency information and contacts" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Launcher Box" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "Add launchers to the lock screen This plug-in is experimental." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Mobile Data Quick Setting" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Toggle mobile data on/off" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Night Light Quick Setting" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Toggle night light on/off" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro Quick Setting" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Simple Pomodoro Timer" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Ticket Box" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Show PDFs on the lock screen. This plug-in is experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Upcoming Events" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Show upcoming calendar events" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Add to Folder" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Create new folder" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Application" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Show All Apps" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Show Only Mobile Friendly Apps" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Battery %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "On" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "No connectable Bluetooth Devices found" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth disabled" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Unknown caller" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wi-Fi network '%s' uses a captive portal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "The Wi-Fi network uses a captive portal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Sign into Wi-Fi network" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Docked" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Undocked" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:22 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Unable to place emergency call" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Internal error" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Log Out" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s will be logged out automatically in %d second." +msgstr[1] "%s will be logged out automatically in %d seconds." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:69 +msgid "Power Off" +msgstr "Power Off" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "The system will power off automatically in %d second." +msgstr[1] "The system will power off automatically in %d seconds." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Restart" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "The system will restart automatically in %d second." +msgstr[1] "The system will restart automatically in %d seconds." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Quiet" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silent" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "On" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Allow '%s' to access your location information?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolocation" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Yes" + +#: src/location-manager.c:274 +msgid "No" +msgstr "No" + +#. give visual feedback on error +#: src/lockscreen.c:311 src/ui/lockscreen.ui:281 +msgid "Enter Passcode" +msgstr "Enter Passcode" + +#: src/lockscreen.c:974 +msgid "Checking…" +msgstr "Checking…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Screenshot saved to '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Failed to save screenshot" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:187 +msgid "Screenshot" +msgstr "Screenshot" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Screenshots" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Screenshot from %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:486 src/ui/media-player.ui:217 +msgid "Unknown Title" +msgstr "Unknown Title" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:494 src/ui/media-player.ui:205 +msgid "Unknown Artist" +msgstr "Unknown Artist" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Built-in display" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Unknown" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Authentication type of Wi-Fi network “%s” not supported" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Enter password for the Wi-Fi network “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Open" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1012 +msgid "Notification" +msgstr "Notification" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "now" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mo" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmo" +msgstr[1] "%dmo" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Over %dy" +msgstr[1] "Over %dy" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Almost %dy" +msgstr[1] "Almost %dy" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Authentication dialogue was dismissed by the user" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:77 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Password:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Sorry, that didn’t work. Please try again." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Portrait" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Landscape" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Off" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "On" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Press ESC to close" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Running '%s' failed" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Phone Shell Volume Control" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Passwords do not match." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Password cannot be blank" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Torch" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Remember decision" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Cancel" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Remove from _Favourites" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Add to _Favourites" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "View _Details" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Uninstall" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Remove from Folder" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Search apps…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Output Devices" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Input Devices" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Sound Settings" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Enable Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth Settings" + +#: src/ui/emergency-menu.ui:24 +msgid "Close the emergency call dialog" +msgstr "Close the emergency call dialogue" + +#: src/ui/emergency-menu.ui:50 +msgid "Emergency _Contacts" +msgstr "Emergency _Contacts" + +#: src/ui/emergency-menu.ui:57 +msgid "Go to the emergency contacts page" +msgstr "Go to the emergency contacts page" + +#: src/ui/emergency-menu.ui:80 +msgid "Go back to the emergency dialpad page" +msgstr "Go back to the emergency dialpad page" + +#: src/ui/emergency-menu.ui:103 +msgid "Owner unknown" +msgstr "Owner unknown" + +#: src/ui/emergency-menu.ui:121 plugins/emergency-info/emergency-info.ui:208 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Emergency Contacts" + +#: src/ui/emergency-menu.ui:139 +msgid "No emergency contacts available." +msgstr "No emergency contacts available." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Some applications are busy or have unsaved work" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Feedback" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Do not disturb" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Feedback Settings" + +#: src/ui/gtk-mount-prompt.ui:77 +msgid "User:" +msgstr "User:" + +#: src/ui/gtk-mount-prompt.ui:99 +msgid "Domain:" +msgstr "Domain:" + +#: src/ui/gtk-mount-prompt.ui:131 +msgid "Co_nnect" +msgstr "Co_nnect" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:244 +msgid "Back" +msgstr "Back" + +#: src/ui/lockscreen.ui:94 +msgid "Slide up to unlock" +msgstr "Slide up to unlock" + +#: src/ui/lockscreen.ui:331 +msgid "Unlock" +msgstr "Unlock" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Authentication required" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Cancel" + +#: src/ui/network-auth-prompt.ui:54 +msgid "C_onnect" +msgstr "C_onnect" + +#: src/ui/polkit-auth-prompt.ui:100 +msgid "Authenticate" +msgstr "Authenticate" + +#: src/ui/power-menu.ui:106 +msgid "Suspend" +msgstr "Suspend" + +#: src/ui/power-menu.ui:149 +msgid "Lock" +msgstr "Lock" + +#: src/ui/power-menu.ui:225 +msgid "Emergency" +msgstr "Emergency" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Run Command" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "No notifications" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Notifications" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Clear all" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirm:" + +#: src/ui/top-panel.ui:31 +msgid "_Power Off…" +msgstr "_Power Off…" + +#: src/ui/top-panel.ui:58 +msgid "_Restart…" +msgstr "_Restart…" + +#: src/ui/top-panel.ui:85 +msgid "_Suspend…" +msgstr "_Suspend…" + +#: src/ui/top-panel.ui:112 +msgid "_Log Out…" +msgstr "_Log Out…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:86 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi Settings" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Plug-in not found" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "The plug-in '%s' could not be loaded." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "No Wi-Fi Device Found" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi Disabled" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Enable Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi Hotspot Active" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Turn Off" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "No Wi-Fi Hotspots" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobile" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh on caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "On" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Off" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Default style" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Dark mode" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Light mode" + +#: plugins/emergency-info/emergency-info.ui:40 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Personal Information" + +#: plugins/emergency-info/emergency-info.ui:48 +msgid "Date of Birth" +msgstr "Date of Birth" + +#: plugins/emergency-info/emergency-info.ui:68 +msgid "Preferred Language" +msgstr "Preferred Language" + +#: plugins/emergency-info/emergency-info.ui:88 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Home Address" + +#: plugins/emergency-info/emergency-info.ui:96 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Medical Information" + +#: plugins/emergency-info/emergency-info.ui:104 +msgid "Age" +msgstr "Age" + +#: plugins/emergency-info/emergency-info.ui:124 +msgid "Blood Type" +msgstr "Blood Type" + +#: plugins/emergency-info/emergency-info.ui:144 +msgid "Height" +msgstr "Height" + +#: plugins/emergency-info/emergency-info.ui:164 +msgid "Weight" +msgstr "Weight" + +#: plugins/emergency-info/emergency-info.ui:184 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergies" + +#: plugins/emergency-info/emergency-info.ui:192 +msgid "Medications & Conditions" +msgstr "Medications & Conditions" + +#: plugins/emergency-info/emergency-info.ui:200 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Other Information" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Emergency Info Preferences" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Done" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Owner Name" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Date of Birth" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Preferred Language" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Age" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Blood Type" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Height" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Weight" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medications and Conditions" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Add Contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Add New Contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Add" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Contact Name" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relationship" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Contact Number" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "No launchers configured" + +#: plugins/launcher-box/launcher-box.ui:30 +msgid "Launchers" +msgstr "Launchers" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Mobile Data On" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Mobile Data Off" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Night Light On" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Night Light Off" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Pomodoro start" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Focus on your task for %d minutes" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Take a break" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "You have %d minutes until next Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Pomodoro Timer" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Off" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro Quick Setting Preferences" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro Technique" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Active Duration" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Duration of the focus session" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Break Duration" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Duration of the break between sessions" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "No documents to display" + +#: plugins/ticket-box/ticket-box.ui:78 +msgid "Tickets" +msgstr "Tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Open" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Choose Folder" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Ticket Box Preferences" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Paths" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Folder Settings" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Where Phosh looks for your tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Ticket Folder" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Today" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Tomorrow" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "In %u day" +msgstr[1] "In %u days" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "No events" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "All day" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Ends" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Untitled event" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Upcoming Events Preferences" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Days" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Date Range" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Number of days to show events for" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Monitor scales" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Hotspot On" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Hotspot Off" + +#~ msgid "Add" +#~ msgstr "Add" + +#~ msgid "Number" +#~ msgstr "Number" + +#~ msgid "Scan" +#~ msgstr "Scan" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mo" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mos" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "y" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "y" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "App" + +#~| msgid "Power Off" +#~ msgid "_Power Off" +#~ msgstr "_Power Off" + +#~ msgid "_Screenshot" +#~ msgstr "_Screenshot" + +#~| msgid "Application" +#~ msgid "Unknown application" +#~ msgstr "Unknown application" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Show only adaptive apps" + +#~ msgid "Lock Screen" +#~ msgstr "Lock Screen" + +#~ msgid "Logout" +#~ msgstr "Logout" + +#~| msgid "Unknown" +#~ msgid "Unknown artist" +#~ msgstr "Unknown artist" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d/%m/%y" + +#~| msgid "Unknown" +#~ msgid "Unknown Song" +#~ msgstr "Unknown Song" diff --git a/po/eo.po b/po/eo.po new file mode 100644 index 000000000..fd05283ad --- /dev/null +++ b/po/eo.po @@ -0,0 +1,1181 @@ +# Esperanto translation for phosh. +# Copyright (C) 2018 Free Software Foundation, Inc. +# This file is distributed under the same license as the phosh package +# Martin CHANG , 2018-2019. +# Colin REEDER , 2021. +# Kristjan SCHMIDT , 2021-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-09-20 15:32+0000\n" +"PO-Revision-Date: 2025-09-20 23:08+0200\n" +"Last-Translator: Kristjan SCHMIDT \n" +"Language-Team: Esperanto \n" +"Language: eo\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 48.0\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Telefona Ŝelo" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Administrado de fenestroj kaj lanĉado de aplikaĵoj por poŝtelefono" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Ĉi tiu seanco ensalutas vin en Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Rapida agordo: Kafeino" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Malebligi, ke la seanco senaktiviĝu" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalendaro" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Simpla kalendara fenestraĵo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Rapida agordo: Malhela reĝimo / kolorskemo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Ŝalti/malŝalti malhelan reĝimon" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Krizinformoj" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Montri krizinformojn kaj kontaktojn" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Lanĉilujo" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "Aldoni lanĉilojn al la ŝlosekrano. Tiu kromprogramo estas eksperimenta." + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Medioludiloj" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Spuri nun rulantajn medioludilojn" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Rapida agordo: Poŝtelefonaj datumoj" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Ŝalti/malŝalti poŝtelefonajn datumojn" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Rapida agordo: Nokta lumo" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Ŝalti/malŝalti noktan lumon" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Rapida agordo: Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Simpla Pomodoro-temporizilo" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Biletujo" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Montri PDF-ojn en la ŝlosekrano. Tiu kromprogramo estas eksperimenta." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Baldaŭaj eventoj" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Montri baldaŭajn kalendarajn eventojn" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Aldoni al dosierujo" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Krei novan dosierujon" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplikaĵo" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Montri ĉiujn aplikaĵojn" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Montri nur por poŝtelefono taŭgajn aplikaĵojn" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Baterio %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bludento" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Ŝaltita" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Neniuj konekteblaj bludentaj aparatoj trovitaj" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bludento malŝaltita" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Nekonata alvokanto" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Vifio-reto '%s' uzas kaptan portalon" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "La vifio-reto uzas kaptan portalon" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Ensaluti al vifio-reto" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Dokita" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Ne dokita" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Bone" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Ne eblas fari krizan alvokon" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Interna eraro" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Elsaluti" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s estos elsalutita aŭtomate post %d sekundo." +msgstr[1] "%s estos elsalutita aŭtomate post %d sekundoj." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Malŝalti" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "La sistemo malŝaltos aŭtomate post %d sekundo." +msgstr[1] "La sistemo malŝaltos aŭtomate post %d sekundoj." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Restartigi" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "La sistemo restartos aŭtomate post %d sekundo." +msgstr[1] "La sistemo restartos aŭtomate post %d sekundoj." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Mallaŭta" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silenta" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Ŝaltita" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Ĉu permesi al '%s' aliri viajn lokinformojn?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolokigo" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Jes" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Ne" + +#. give visual feedback on error +#: src/lockscreen.c:313 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Enigu pasvorton" + +#: src/lockscreen.c:956 +msgid "Checking…" +msgstr "Kontroli…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Ekrankopio konservita al '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Malsukcesis konservi ekrankopion" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Ekrankopio" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Ekrankopioj" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Ekrankopio de %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:646 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Nekonata titolo" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:654 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Nekonata artisto" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Enkonstruita ekrano" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Nekonata" + +#: src/network-auth-prompt.c:202 +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Tipo de aŭtentigo de vifio-reto “%s” ne estas subtenata" + +#: src/network-auth-prompt.c:207 +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Enigu pasvorton por la vifio-reto “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Malfermi" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1012 +msgid "Notification" +msgstr "Sciigo" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "nun" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mo" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmo" +msgstr[1] "%dmos" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Pli ol %dj" +msgstr[1] "Pli ol %dj" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Preskaŭ %dj" +msgstr[1] "Preskaŭ %dj" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "La aŭtentiga dialogo estis forigita de la uzanto" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Pasvorto:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Pardonu, ĉi tio ne sukcesas. Bonvolu reprovi." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Vertikala" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Horizontala" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Malŝaltita" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Ŝaltita" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Premu ESC por fermi" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Rulo de '%s' malsukcesis" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Laŭtecregilo de telefona ŝelo" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Pasvortoj ne kongruas." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Pasvorto ne povas esti malplena" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Poŝlampo" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Memori decidon" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Nuligi" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Forigi el _Plej ŝatataj" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Aldoni al _Plej ŝatataj" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Vidi _detalojn" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Malinstali" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Fo_rigi el dosierujo" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Serĉi aplikaĵojn…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Eliraj aparatoj" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Eniraj aparatoj" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Sonaj agordoj" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Ebligi bludenton" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Agordoj de bludento" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Fermi la dialogon pri kriza alvoko" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Kriz_kontaktoj" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Iri al la paĝo de krizkontaktoj" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Reiri al la paĝo de kriza numerklavaro" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Nekonata posedanto" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Krizkontaktoj" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Neniuj krizkontaktoj disponeblas." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Kelkaj aplikaĵoj estas okupita aŭ havas nekonservitajn ŝanĝojn" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Reago" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Ne ĝenu" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Agordoj de reago" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Uzanto:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domajno:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Ko_nekti" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Reen" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Ŝovi supre por malŝlosi" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Malŝlosi" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Aŭtentigo estas deviga" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Nuligi" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "K_onekti" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Aŭtentigi" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Suspendi" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Ŝlosi" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Krizo" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Ruli komandon" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Neniuj sciigoj" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Sciigoj" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Forigi ĉion" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Konfirmi:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Malŝalti…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Restartigi…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspendi…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "E_lsaluti…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Vifio" + +#: src/ui/wifi-status-page.ui:89 +msgid "Wi-Fi Settings" +msgstr "Agordoj de vifio" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Kromprogramo ne trovita" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "La kromprogramo '%s' ne povis esti ŝargita." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Neniu vifio-aparato trovita" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Vifio malŝaltita" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Ebligi vifion" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Vifia alirpunkto aktiva" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Malŝalti" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Neniuj vifiaj alirpunktoj" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Ĉela" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh sur kafeino" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Ŝaltita" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Malŝaltita" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Defaŭlta stilo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Malhela reĝimo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Hela reĝimo" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Personaj informoj" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Naskiĝdato" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Preferata lingvo" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Hejmadreso" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Medicininformoj" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Aĝo" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Sangogrupo" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Alteco" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Pezo" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergioj" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medikamentoj kaj kondiĉoj" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Aliaj informoj" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferoj de krizinformoj" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Preta" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Nomo de posedanto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Naskiĝdato" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Preferata lingvo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Aĝo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Sangogrupo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Alteco" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Pezo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medikamentoj kaj kondiĉoj" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Aldoni kontakton" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Aldoni novan kontakton" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Aldoni" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Nomo de kontakto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Rilato" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Kontaktnumero" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Neniuj lanĉiloj agorditaj" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Lanĉiloj" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Neniuj rulantaj medioludiloj" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:82 +msgid "Mobile Data On" +msgstr "Poŝtelefonaj datumoj ŝaltitaj" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:82 +msgid "Mobile Data Off" +msgstr "Poŝtelefonaj datumoj malŝaltitaj" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Nokta lumo ŝaltita" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Nokta lumo malŝaltita" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:75 +msgid "Pomodoro start" +msgstr "Komenco de Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:76 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Fokusiĝu je via tasko dum %d minutoj" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:81 +msgid "Take a break" +msgstr "Faru paŭzon" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:83 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Vi havas %d minutojn ĝis la sekva Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:98 +msgid "Pomodoro Timer" +msgstr "Temporizilo de Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:121 +msgid "Pomodoro Off" +msgstr "Pomodoro malŝaltita" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Preferoj de rapida agordo de Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Tekniko Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Aktiva daŭro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Daŭro de la fokusa seanco" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Paŭza daŭro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Daŭro de la paŭzo inter seancoj" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Starti je malŝloso" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Ĉu startigi la temporizilon je malŝloso de ekrano" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Neniuj dokumentoj por montri" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Biletoj" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Malfermi" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Elekti dosierujon" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferoj de Biletujo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Vojoj" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Agordoj de dosierujo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Kie Phosh serĉas viajn biletojn" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Biletujo" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hodiaŭ" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Morgaŭ" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Neniuj eventoj" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tuttaga" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Finiĝas" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Sennoma evento" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferoj de Baldaŭaj Eventoj" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Tagoj" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Data intervalo" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Nombro da tagoj por montri eventojn" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Skaloj de ekrano" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Alirpunkto ŝaltita" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Alirpunkto malŝaltita" + +#~ msgid "Unknown application" +#~ msgstr "Nekonata aplikaĵo" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "t" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "t" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mo" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mo" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "j" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "j" + +#~ msgid "App" +#~ msgstr "Aplikaĵo" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Montri nur adapta aplikaĵojn" + +#~ msgid "Lock Screen" +#~ msgstr "Ŝlosita Ekrano" + +#~ msgid "Logout" +#~ msgstr "Elsaluti" diff --git a/po/es.po b/po/es.po new file mode 100644 index 000000000..82f7ea3b7 --- /dev/null +++ b/po/es.po @@ -0,0 +1,758 @@ +# Spanish translation for Phosh. +# Copyright (C) 2022 Phosh developers +# This file is distributed under the same license as the phosh package. +# +# Roberto MF , 2018. #zanata +# Amos Batto , 2019. #zanata +# Maximilian , 2019. #zanata +# Roberto MF , 2019. #zanata +# Daniel Mustieles , 2020. +# carlosgonz , 2022. +# Pablo Barciela , 2022-2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-06-20 08:29+0000\n" +"PO-Revision-Date: 2023-06-20 10:51+0200\n" +"Last-Translator: Pablo Barciela \n" +"Language-Team: Spanish - Spain \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 42.0\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Interfaz para Móviles" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Gestor de ventanas y lanzador de aplicaciones para móviles" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Esto inicia su sesión en Phosh" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendario" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Un sencillo widget de calendario" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Caja de tickets" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Muestra PDF en la pantalla de bloqueo. Este complemento es experimental" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Próximos eventos" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Muestra los próximos eventos del calendario" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Aplicación" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Mostrar Todas las Aplicaciones" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostrar Sólo Aplicaciones Adaptadas" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Activado" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Acoplado" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desacoplado" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "Aceptar" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "No ha sido posible realizar la llamada de emergencia" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Error interno" + +#: src/end-session-dialog.c:163 +msgid "Log Out" +msgstr "Cerrar la sesión" + +#: src/end-session-dialog.c:166 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s se deslogueará automáticamente en %d segundo." +msgstr[1] "%s se deslogueará automáticamente en %d segundos." + +#: src/end-session-dialog.c:172 +msgid "Power Off" +msgstr "Apagar" + +#: src/end-session-dialog.c:173 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "El sistema se apagará automáticamente en %d segundo." +msgstr[1] "El sistema se apagará automáticamente en %d segundos." + +#: src/end-session-dialog.c:179 +msgid "Restart" +msgstr "Reiniciar" + +#: src/end-session-dialog.c:180 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "El sistema se reiniciará automáticamente en %d segundo." +msgstr[1] "El sistema se reiniciará automáticamente en %d segundos." + +#: src/end-session-dialog.c:270 +msgid "Unknown application" +msgstr "Aplicación desconocida" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Vibración" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silenciado" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Activado" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "¿Permitir que '%s' acceda a la información de ubicación?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolocalización" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Sí" + +#: src/location-manager.c:274 +msgid "No" +msgstr "No" + +#: src/lockscreen.c:170 src/ui/lockscreen.ui:221 +msgid "Enter Passcode" +msgstr "Introduce el código de desbloqueo" + +#: src/lockscreen.c:393 +msgid "Checking…" +msgstr "Verificando…" + +#: src/screenshot-manager.c:212 +msgid "Screenshot" +msgstr "Captura de pantalla" + +#: src/screenshot-manager.c:213 +msgid "Screenshot copied to clipboard" +msgstr "Captura de pantalla copiada al portapapeles" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:182 +msgid "Unknown Title" +msgstr "Título Desconocido" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:165 +msgid "Unknown Artist" +msgstr "Artista Desconocido" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Monitor incorporado" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Desconocido" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "No se admite el tipo de autenticación wifi de “%s”" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Introducir contraseña wifi de “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Abrir" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "Notificación" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "ms" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "h" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "hs" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "ds" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "me" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mes" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "a" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "as" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "Ahora" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Sobre %da" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Casi %da" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "El usuario descartó el cuadro de diálogo de autenticación" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Contraseña:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Lo siento, eso no funcionó. Inténtelo de nuevo." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Vertical" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Horizontal" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desactivada" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Activada" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Pulse ESC para cerrar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "El comando '%s' falló" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Las contraseñas no coinciden." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "La contraseña no puede estar vacía" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Linterna" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Recordar decisión" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +msgid "Cancel" +msgstr "Cancelar" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "App" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Quitar de _Favoritos" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Agregar a _Favoritos" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Ver _detalles" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Buscar apps…" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Cerrar el diálogo de las llamadas de emergencia" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Contactos de emergencia" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Ir a la página de contactos de emergencia" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Volver a página de tecleo de número de emergencia" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Propietario desconocido" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +msgid "Emergency Contacts" +msgstr "Contactos de emergencia" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Ningún contacto de emergencia disponible" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Algunas aplicaciones están ocupadas o tienen trabajo sin guardar" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Usuario:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Dominio:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "Co_nectar" + +#: src/ui/lockscreen.ui:39 src/ui/lockscreen.ui:310 +msgid "Back" +msgstr "Atrás" + +#: src/ui/lockscreen.ui:90 +msgid "Slide up to unlock" +msgstr "Deslice hacia arriba para desbloquear" + +#: src/ui/lockscreen.ui:273 +msgid "Unlock" +msgstr "Desbloquear" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Autenticación requerida" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Cancelar" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "C_onectar" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Autenticar" + +#: src/ui/power-menu.ui:69 +msgid "_Power Off" +msgstr "_Apagar" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "B_loquear" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "_Capturar" + +#: src/ui/power-menu.ui:192 +msgid "_Emergency" +msgstr "_Emergencia" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Ejecutar comando" + +#: src/ui/settings.ui:290 +msgid "No notifications" +msgstr "Ninguna notificación" + +#: src/ui/settings.ui:330 +msgid "Clear all" +msgstr "Eliminar todos" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Confirmar:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "_Apagar" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "_Reiniciar" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Cerrar la sesión" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +msgid "%A, %B %-e" +msgstr "%A, %d de %B" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Complemento no encontrado" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "No se pudo cargar el complemento '%s'" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Módem" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Información personal" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Fecha de nacimiento" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Idioma de preferencia" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Dirección" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "Información médica" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "Edad" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "Tipo sanguíneo" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "Altura" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "Peso" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "Alergias" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Condición médica y tratamientos" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "Otra información" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Ajustes de información de emergencia" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Hecho" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Nombre del propietario" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "Condición médica y tratamientos" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "Añadir contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "Añadir nuevo contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "Añadir" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:315 +msgid "New Contact Name" +msgstr "Nombre del nuevo contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:329 +msgid "Relationship" +msgstr "Relación" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:342 +msgid "Number" +msgstr "Número" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "No hay documentos que mostrar" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Seleccione carpeta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Abrir" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferencias de la caja de tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Rutas" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Ajustes de carpeta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Dónde debe Phosh buscar sus tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Carpeta de tickets" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hoy" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Mañana" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "En %d día" +msgstr[1] "En %d días" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Sin eventos" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Todo el día" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Finaliza" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Evento sin título" + +#~ msgid "Emergency" +#~ msgstr "Emergencia" diff --git a/po/eu.po b/po/eu.po new file mode 100644 index 000000000..b23622da6 --- /dev/null +++ b/po/eu.po @@ -0,0 +1,1299 @@ +# alarra , 2019. #zanata +msgid "" +msgstr "Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-12-15 18:28+0000\n" +"PO-Revision-Date: 2025-12-03 12:37+0200\n" +"Last-Translator: Asier Saratsua Garmendia \n" +"Language-Team: Basque \n" +"Language: eu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Shell mugikorra" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Leihoen kudeaketa eta aplikazioen abioa mugikorrentzako" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Saio honek Phosh aplikazioan sartuko zaitu" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Kafeina-moduaren ezarpen azkarra" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Eragotzi saioa inaktibo gera dadin" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Egutegia" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Egutegi-trepeta xume bat" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Modu ilunaren / Kolore-eskemaren ezarpen azkarra" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Txandakatu modu iluna" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Larrialdi-informazioa" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Erakutsi larrialdi-informazioa eta -kontaktuak" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Abiarazle-kutxa" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "Gehitu abiarazleak blokeo-pantailari. Plugin hau esperimentala da." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Kokalekuaren ezarpen azkarra" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Aktibatu/Desaktibatu kokaleku-zerbitzuak" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Multimedia-erreproduzigailuak" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Jarraitu unean abioan dauden multimedia-erreproduzitzaileak" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Datu mugikorren ezarpen azkarra" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Aktibatu/Desaktibatu datu mugikorrak" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Gaueko argiaren ezarpen azkarra" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Aktibatu/Desaktibatu gaueko argia" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Pomodororen ezarpen azkarra" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Pomodoro tenporizadore sinplea" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Txartel-kutxa" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Erakutsi PDFak blokeo-pantailan. Plugin hau esperimentala da." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Etorkizuneko gertaerak" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Erakutsi etorkizuneko egutegi-gertaerak" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Gehitu karpetari" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Sortu karpeta berria" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplikazioa" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Erakutsi aplikazio guztiak" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Erakutsi mugikorrean ongi doazen aplikazioak soilik" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Bateria %%%.0f" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetootha" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Aktibatuta" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Ez da aurkitu konektatu daitekeen Bluetooth gailurik" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetootha desaktibatuta" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Dei-egile ezezaguna" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "'%s' wifi-sareak atari gatibua darabil" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wifi sareak atari gatibua darabil" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Hasi saioa wifi-sarean" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Atrakatuta" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desatrakatuta" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Ados" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Ezin dira larrialdi-deiak kokatu" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Barneko errorea" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Amaitu saioa" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s erabiltzailearen saioa automatikoki segundo %dean amaituko da." +msgstr[1] "%s erabiltzailearen saioa automatikoki %d segundotan amaituko da." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Itzali" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistema automatikoki segundo %dean itzaliko da." +msgstr[1] "Sistema automatikoki %d segundotan itzaliko da." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Berrabiarazi" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistema automatikoki segundo %dean berrabiaraziko da." +msgstr[1] "Sistema automatikoki %d segundotan berrabiaraziko da." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Geldi" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Isilik" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Aktibatuta" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Onartu '%s' aplikazioak zure kokalekuaren informazioa eskuratu dezan?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Geolokalizazioa" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Bai" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Ez" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Sartu pasakodea" + +#: src/lockscreen.c:1040 +msgid "Checking…" +msgstr "Egiaztatzen…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Pantaila-argazkia hemen gorde da: '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Huts egin du pantaila-argazkia gordetzeak" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Pantaila-argazkia" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Pantaila-argazkiak" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Pantaila-argazkia: %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Titulu ezezaguna" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Artista ezezaguna" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Pantaila integratuta" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Ezezaguna" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "“%s” wifi-sarearen autentifikazio mota ez da onartzen" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Sartu “%s” wifi-sarearen pasahitza" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Ireki" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Jakinarazpena" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "orain" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%do" +msgstr[1] "~%do" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1e" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%de" +msgstr[1] "%de" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1h" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dh" +msgstr[1] "%dh" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%du" +msgstr[1] "~%du" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%du baino gehiago" +msgstr[1] "%du baino gehiago" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Ia %du" +msgstr[1] "Ia %du" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Erabiltzaileak autentifikazio-koadroa baztertu du" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Pasahitza:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Barkatu, horrek ez du funtzionatu. Saiatu berriro." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Bertikala" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Horizontala" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desaktibatuta" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Aktibatuta" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Sakatu ESC ixteko" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "'%s' exekutatzeak huts egin du" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Shell mugikorraren bolumen-kontrola" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Pasahitzak ez datoz bat." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Pasahitza ezin da hutsik egon" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Torch" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Gogoratu erabakia" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Utzi" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Kendu g_ogokoetatik" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Gehitu _gogokoei" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Ikusi _xehetasunak" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Desinstalatu" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Kendu karpetatik" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Bilatu aplikazioak…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Irteerako gailuak" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Sarrerako gailuak" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Soinu-ezarpenak" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Distira automatikoa" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Distiraren ezarpenak" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Gaitu Bluetootha" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetootharen ezarpenak" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Itxi larrialdi-deiaren koadroa" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Larrialdiko _kontaktuak" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Joan larrialdiko kontaktuen orrira" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Itzuli larrialdi-deirako teklaturaren orrira" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Jabe ezezaguna" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Larrialdietarako kontaktuak" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Ez dago larrialdiko kontakturik eskuragarri." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Aplikazio batzuk lanpetuta daude edo gorde gabeko lanak dituzte." + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Ez gogaitu" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Erabiltzailea:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domeinua:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Konektatu" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Atzera" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Irristatu gora desblokeatzeko" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Desblokeatu" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autentifikazioa behar da" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Utzi" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Konektatu" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Autentifikatu" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Eseki" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Blokeatu" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Larrialdia" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Exekutatu komandoa" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Jakinarazpenik ez" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Jakinarazpenak" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Garbitu dena" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Berretsi:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Itzali…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Berrabiarazi…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Eseki…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Amaitu _saioa…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wifia" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Wifiaren ezarpenak" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPNa" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Ez da plugina aurkitu" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Ezin izan da '%s' plugina kargatu." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Ez da wifi-gailurik aurkitu" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wifia desgaitu da" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Gaitu wifia" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wifigunea aktibo" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Desaktibatu" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Ez dago wifigunerik" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mugikorra" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh kafeinarekin" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Desaktibatuta" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Aktibatuta" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Kafeina-tenporizadoreak" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "Ez dago kafeina-tarterik" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Ez dago denbora-mugarik(∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Kafeinaren iraupena" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Kudeatu kafeinaren iraupena" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Gehitu edo kendu kafeina-tarte pertsonalizatuak" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Gehitu tartea" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Gehitu tarte berria" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Gehitu" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Abio azkarraren tarteak" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 o" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Aukeratu tartea" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Estilo lehenetsia" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Modu iluna" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Modu argia" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Datu pertsonalak" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Jaiotze-data" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Hizkuntza hobetsia" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Etxeko helbidea" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informazio medikoa" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Adina" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Odol mota" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Altuera" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Pisua" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergiak" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medikazioa eta gaixotasunak" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Informazio gehigarria" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Larrialdi-informazioaren hobespenak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Egina" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Jabearen izena" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "J_aiotze-data" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Hizkuntza hobetsia" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "A_dina" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Odol mota" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "A_ltuera" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "P_isua" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medikazioa eta gaixotasunak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Gehitu kontaktua" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Gehitu kontaktu berria" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Kontaktuaren izena" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Erlazioa" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Kontaktuaren zenbakia" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Ez da abiarazlerik konfiguratu" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Abiarazleak" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Kokalekua aktibatuta" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Kokalekua desaktibatuta" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Ez dago multimedia-erreproduzigailurik exekuzioan" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Datu mugikorrak aktibatuta" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Datu mugikorrak desaktibatuta" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Gaueko argia aktibatuta" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Gaueko argia desaktibatuta" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodororen hasiera" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Jarri arreta lanean %d minutuz" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Egin etenaldi bat" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "%d minutu geratzen dira hurrengo Pomodorora arte" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Pomodoo tenporizadorea" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro desaktibatuta" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro teknika" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Iraupen _aktiboa" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Arreta-aldiaren iraupena" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Etenaldiaren iraupena" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Saioen arteko etenaldiaren iraupena" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Ez dago dokumenturik bistaratzeko" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Txartelak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Ireki" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Hautatu karpeta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Txartel-kutxaren hobespenak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Bide-izenak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Karpeta-ezarpenak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Phosh aplikazioak non bilatuko dituen txartelak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Txartel-karpeta" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Gaur" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Bihar" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Gertaerarik ez" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Egun osoa" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Amaiera-data" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Izenik gabeko gertaera" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Ez dago gertaerarik hurrengo %d egunetarako" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Ez dago gertaerarik etorkizunean" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Etorkizuneko gertaeren hobespenak" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Egunak" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Data-tartea" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Gertaerak zenbat egunetarako erakutsiko diren" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%% %d" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Wifigunea" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Aktibatu" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Wifigunea aktibatuta" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Wifigunea desaktibatuta" + +#~ msgid "Unknown application" +#~ msgstr "Aplikazio ezezaguna" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "o" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "o" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "e" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "e" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "u" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "u" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplikazioa" + +#~ msgid "_Power Off" +#~ msgstr "_Itzali" + +#~ msgid "_Screenshot" +#~ msgstr "_Pantaila-argazkia" + +#~ msgid "Number" +#~ msgstr "Zenbakia" + +#, c-format +#~ msgid "In %d day" +#~ msgid_plural "In %d days" +#~ msgstr[0] "Egun %dean" +#~ msgstr[1] "%d egunetan" + +#~ msgid "Lock Screen" +#~ msgstr "Blokeatu pantaila" + +#~ msgid "Logout" +#~ msgstr "Amaitu saioa" diff --git a/po/fa.po b/po/fa.po new file mode 100644 index 000000000..760d8fbc3 --- /dev/null +++ b/po/fa.po @@ -0,0 +1,1239 @@ +# Persian translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Danial Behzadi , 2020-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-04 16:18+0330\n" +"Last-Translator: Danial Behzadi \n" +"Language-Team: Persian\n" +"Language: fa\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.8\n" +"X-DL-Lang: fa\n" +"X-DL-Module: phosh\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-State: Translating\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "پوستهٔ تلفن" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "مدیریت پنجره‌ها و اجرا کنندهٔ برنامه‌ها برای تلفن همراه" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "فوش" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "این نشست وارد فوشتان می‌کند" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "تنظیمات سریع کافئین" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "جلوگیری از بی‌کار شدن نشست" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "تقویم" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "ابزارک تقویمی ساده" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "تنظیمات سریع حالت تیره و طرحوارهٔ رنگی" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "تغییر وضعیت حالت تیره" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "اطّلاعات اضطراری" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "نمایش آشنایان و اطّلاعات اضطراری" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "جعبهٔ اجراگر" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "افزودن اجراگرها به صفحهٔ قفل. این افزایه آزمایشی است." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "تنظیمات سریع مکان" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "تغییر وضعیت خدمات مکانی" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "پخش‌کننده‌های آهنگ" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "ردیابی پخش کننده‌های رسانهٔ در حال اجرا" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "تنظیمات سریع دادهٔ همراه" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "تغییر وضعیت دادهٔ همراه" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "تنظیمات سریع نور شب" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "تغییر وضعیت نور شب" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "تنظیمات سریع پومودورو" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "زمان‌سنج پومودوروی ساده" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "جعبهٔ بلیت" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "نمایش PDFها روی صفحهٔ قفل. این افزایه آزمایشی است." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "رویدادهای پیش رو" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "نمایش رویدادهای تقویم پیش رو" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "افزودن به شاخه" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "ایجاد شاخهٔ جدید" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "برنامه" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "نمایش تمامی کاره‌ها" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "فقط نمایش کاره‌های سازگار با تلفن همراه" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "واپایش حجم صدای پوستهٔ تلفن" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "باتری %I.0f٪" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "بلوتوث" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "روشن" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "هیچ افزارهٔ وصل شدنی بلوتوثی پیدا نشد" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "بلوتوث از کار افتاده" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "تماس‌گیرندهٔ ناشناس" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "شبکهٔ وای‌فای «%s» از درگاهی گیرنده استفاده می‌کند" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "شبکهٔ وای‌فای از درگاهی گیرنده استفاده می‌کند" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "ورود به شبکهٔ وای‌فای" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "داک شده" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "داک نشده" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "قبول" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "ناتوان در برقراری تماس اضطراری" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "خطای داخلی" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "خروج" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s به طور خودکار در %Id ثانیه از سامانه خارج خواهد شد." +msgstr[1] "%s به طور خودکار در %Id ثانیه از سامانه خارج خواهد شد." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "خاموش کردن" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "سامانه پس از %Id ثانیه به طور خودکار خاموش می‌شود." +msgstr[1] "سامانه پس از %Id ثانیه به طور خودکار خاموش می‌شود." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "راه‌اندازی دوباره" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "سامانه پس از %Id ثانیه به طور خودکار دوباره راه‌اندازی می‌شود." +msgstr[1] "سامانه پس از %Id ثانیه به طور خودکار دوباره راه‌اندازی می‌شود." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "کم‌صدا" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "بی‌صدا" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "روشن" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "اجازه به %s برای دسترسی به اطّلاعات مکانیتان؟" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "مکان جغرافیایی" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "بله" + +#: src/location-manager.c:272 +msgid "No" +msgstr "نه" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "ورود رمز عبور" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "در حال بررسی…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "نماگرفت در %s ذخیره شد" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "شکست در ذخیرهٔ نماگرفت" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "نماگرفت" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "نماگرفت‌ها" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "نماگرفت از ‪%s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "عنوان ناشناخته" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "هنرمند ناشناخته" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "نمایشگر توکار" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "ناشناخته" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "گونهٔ تأییدیهٔ شبکهٔ وای‌فای %s پشتیبانی نمی‌شود" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "ورود گذرواژه برای شبکهٔ وای‌فای %s" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "گشودن" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "آگاهی" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "اکنون" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<۳۰ث" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<۱د" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~۱د" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%Idد" +msgstr[1] "%Idد" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%Idس" +msgstr[1] "~%Idس" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~۱ر" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%Idر" +msgstr[1] "%Idر" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~۱م" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%Idم" +msgstr[1] "%Idم" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%Idسا" +msgstr[1] "~%Idسا" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "بیش از %Idسا" +msgstr[1] "بیش از %Idسا" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "تقریباً %Idسا" +msgstr[1] "تقریباً %Idسا" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "گفت‌وگوی تأیید هویت از سمت کاربر رد شد" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "گذرواژه:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "متاسفانه اثری نداشت! لطفاً دوباره تلاش کنید." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "عمودی" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "افقی" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "خاموش" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "روشن" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "برای بستن، گریز را بزنید" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "اجرای «%s» شکست خورد" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "گذرواژه‌های مطابق نیستند." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "گذرواژه نمی‌تواند خالی باشد" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "چراغ" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "به‌خاطر سپردن تصمیم" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "لغو" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "برداشتن از _محبوب‌ها" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "افزودن به _محبوب‌ها" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "دیدن _جزییات" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "حذف نصب" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_برداشتن از شاخه" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "جست‌وجوی کاره‌ها…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "افزاره‌های خروجی" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "افزاره‌های ورودی" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "تنظیمات صدا" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "روشنایی خودکار" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "تنظیمات روشنایی" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "به کار انداختن بلوتوث" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "تنظیمات بلوتوث" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "بستن گفت‌وگوی تماس اضطراری" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_تماس‌های اضطراری" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "رفتن به صفحهٔ تماس‌های اضطراری" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "بازگشت به صفحهٔ شماره‌گیر اضطراری" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "مالک ناشناخته" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "تماس‌های اضطراری" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "هیچ تماس اضطراری‌ای موجود نیست." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "برخی برنامه‌ها مشغول بوده یا کار ذخیره نشده دارند" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "بازخورد" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "مزاحم نشوید" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "تنظیمات بازخورد" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "کاربر:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "دامنه:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "اتّ_صال" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "بازگشت" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "برای قفل‌گشایی، بالا بکشید" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "قفل‌گشایی" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "نیاز به تأیید هویت است" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_لغو" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_وصل شدن" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "تأیید هویت" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "تعلیق" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "قفل" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "اضطراری" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "اجرای فرمان" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "بدون آگاهی" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "آگاهی‌ها" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "پاک‌سازی همه" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "تأیید:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_خاموش…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_راه‌اندازی دوباره…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_تعلیق…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_خروج…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "وای‌فای" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "تنظیمات وای‌فای" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "وی‌پی‌ان" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A، %Od %OB" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "افزایه پیدا نشد" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "افزایهٔ «%s» نتوانست بار شود." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "هیچ افزارهٔ وای‌فایی پیدا نشد" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "وای‌فای از کار افتاده" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "به کار انداختن وای‌فای" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "نقطهٔ داغ وای‌فای فعّال" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "خاموش کردن" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "بدون نقطهٔ داغ وای‌فای" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "سلولی" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "فوش روی کافئین" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "خاموش" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "روشن" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "شمارش معکوس کافئین" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "بدون دورهٔ کافئین" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "تنظیمات کافئین" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "بدون مهلت زمانی (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "ترجیحات تنظیمات سریع کافئین" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "مدّت کافئین" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "مدیریت مدّت کافئین" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "افزودن یا برداشتن دورهٔ سفارشی کافئین" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "افزودن دوره" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "افزودن دورهٔ جدید" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_افزودن" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "دوره‌های آغاز سریع" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "۵ د" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "۱۵ د" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "۳۰ د" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "۱ س" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "گزینش دوره" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "سبک پیش‌گزیده" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "حالت تیره" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "حالت روشن" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "اطّلاعات شخصی" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "تاریخ تولّد" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "زبان ترجیحی" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "نشانی خانه" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "اطّلاعات پزشکی" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "سن" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "گروه خونی" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "قد" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "وزن" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "حسّاسیت‌ها" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "داروها و شرایط" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "دیگر اطّلاعات" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "ترجیحات اطّلاعات اضطراری" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "انجام شد" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_نام مالک" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_تاریخ تولّد" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_زبان ترجیحی" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_سن" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_گروه خونی" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_قد" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_وزن" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "داروها و شرایط" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "افزودن آشنا" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "افزودن آشنای جدید" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_نام آشنا" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "رابطه" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_شمارهٔ آشنا" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "هیچ اجراگری پیکربندی نشده" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "اجراگرها" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "مکان روشن" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "مکان خاموش" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "هیچ پخش‌کنندهٔ آهنگی در حال اجرا نیست" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "دادهٔ همراه روشن" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "دادهٔ همراه خاموش" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "نور شب روشن" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "نور شب خاموش" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "آغاز پومودورو" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "تمرکز روی وظیفه‌تان برای %Id دقیقه" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "استراحت کنید" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "تا پومودوروی بعد %Id دقیقه دارید" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "زمان‌سنج پومودورو" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "پومودورو خاموش" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "ترجیحات تنظیمات سریع پومودورو" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "فن پومودورو" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "مدّت _فعّال بودن" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "مدّت نشست تمرکز" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "مدّت _استراحت" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "مدّت استراحت بین نشست‌ها" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_آغاز با قفل‌گشایی" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "این که زمان‌سنج با قفل‌گشایی صفحه آغاز شود یا نه" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "هیچ سندی برای نمایش نیست" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "بلیت‌ها" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_گشودن" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "گزینش شاخه" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "ترجیحات جعبهٔ بلیت" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "مسیرها" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "تنظیمات شاخه" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "جایی که فوش به دنبال بلیت‌هایتان می‌گردد" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "شاخهٔ بلیت" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "امروز" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "فردا" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "بدون رویداد" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%OR" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%Ol:%OM %Op" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "تمام روز" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "پایان" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "رویداد بی‌عنوان" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "بدون رویداد در %Id روز آینده" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "بدون رویداد پیشِ رو" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "ترجیحات رویدادهای پیش رو" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "روز" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "بازهٔ زمانی" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "تعداد روز برای نمایش رویدادها" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "مقیاس‌های نمایشگر" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%Id٪" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "نقطهٔ داغ وای‌فای" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "روشن کردن" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "نقطهٔ داغ روشن" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "نقطهٔ داغ خاموش" diff --git a/po/fi.po b/po/fi.po new file mode 100644 index 000000000..b172c4621 --- /dev/null +++ b/po/fi.po @@ -0,0 +1,1332 @@ +# M J , 2019. #zanata +# Jiri Grönroos , 2020 +# JR-Fi , 2019-2020. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-11-29 20:57+0000\n" +"PO-Revision-Date: 2025-11-30 13:00+0200\n" +"Last-Translator: Jiri Grönroos \n" +"Language-Team: Finnish\n" +"Language: fi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Puhelimen käyttöliittymä" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "" +"Ikkunointijärjestelmän hallinta ja ohjelmien käynnistys mobiililaitteille" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Tämä istunto kirjaa sinut Phoshiin" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Caffeine-pika-asetus" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Estä istuntoa menemästä jouten" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalenteri" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Yksinkertainen kalenterisovelma" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Tumman tilan / väriteeman pika-asetus" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Tumma tila päällä/pois" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Hätätilatiedot" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Näytä hätätilatiedot ja -yhteystiedot" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Käynnistinlaatikko" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Lisää käynnistimiä lukitusnäytölle. Tämä liitännäinen on kokeellinen." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Paikannuksen pika-asetus" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Paikannuspalvelu päällä/pois" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Mediasoittimet" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Seuraa käynnissä olevia mediasoittimia" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Mobiilidatan pika-asetus" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Mobiilidata päällä/pois" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Yövalon pika-asetus" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Yövalo päällä/pois" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoron pika-asetus" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Yksinkertainen Pomodoro-ajastin" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Tikettilaatikko" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Näytä PDF-tiedostoja lukitusnäytöllä. Tämä liitännäinen on kokeellinen." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Tulevat tapahtumat" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Näytä tulevat kalenteritapahtumat" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Lisää kansioon" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Luo uusi kansio" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Sovellus" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Näytä kaikki sovellukset" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Näytä vain mobiilille suunnatut sovellukset" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Akku %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Päällä" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Yhdistettävissä olevia Bluetooth-laitteita ei löytynyt" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth pois käytöstä" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Tuntematon soittaja" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wi-Fi-verkko '%s' käyttää kirjautumisportaalia" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wi-Fi-verkko käyttää kirjautumisportaalia" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Kirjaudu Wi–Fi-verkkoon" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Telakassa" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Ei telakassa" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Hätäpuhelu ei onnistunut" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Sisäinen virhe" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Kirjaudu ulos" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s kirjataan ulos automaattisesti %d sekunnin kuluttua." +msgstr[1] "%s kirjataan ulos automaattisesti %d sekunnin kuluttua." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Sammuta" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Järjestelmä sammuu automaattisesti %d sekunnin kuluttua." +msgstr[1] "Järjestelmä sammuu automaattisesti %d sekunnin kuluttua." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Käynnistä uudelleen" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "" +"Järjestelmä käynnistyy uudelleen automaattisesti %d sekunnin kuluttua." +msgstr[1] "" +"Järjestelmä käynnistyy uudelleen automaattisesti %d sekunnin kuluttua." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Äänetön" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Hiljainen" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Päällä" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Voiko '%s' käyttää sijaintitietoasi?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Paikannus" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Kyllä" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Ei" + +#. give visual feedback on error +#: src/lockscreen.c:313 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Syötä salasana" + +#: src/lockscreen.c:956 +msgid "Checking…" +msgstr "Tarkistetaan…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Kuvakaappaus tallennettu tiedostoon '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Kuvakaappauksen tallentaminen epäonnistui" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Kuvakaappaus" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Kuvakaappaukset" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Kuvakaappaus %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Tuntematon kappale" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Tuntematon esittäjä" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Sisäänrakennettu näyttö" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Tuntematon" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Wi-Fi-verkon “%s” tunnistautumistyyppi ei ole tuettu" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Anna salasana Wi-Fi-verkolle “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Avaa" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Ilmoitus" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "nyt" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d min" +msgstr[1] "%d min" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d t" +msgstr[1] "~%d t" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 pv" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d pv" +msgstr[1] "%d pv" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 kk" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d kk" +msgstr[1] "%d kk" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d v" +msgstr[1] "~%d v" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Yli %d v" +msgstr[1] "Yli %d v" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Melkein %d v" +msgstr[1] "Melkein %d v" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Tunnistautumisikkuna peruttu käyttäjän toimesta" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Salasana:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Valitettavasti se ei toiminut. Ole hyvä ja yritä uudelleen." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Pystysuunta" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Vaakasuunta" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Pois" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Päällä" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Paina ESC sulkeaksesi" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Kohteen '%s' suorittaminen epäonnistui" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Puhelimen käyttöliittymän äänihallinta" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Salasanat eivät täsmää." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Salasana ei voi olla tyhjä" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Taskulamppu" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Muista valinta" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Peru" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Poista _suosikeista" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Lisää _suosikkeihin" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Näytä yk_sityiskohdat" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Poista asennus" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Poista kansiosta" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Etsi sovelluksia…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Ulostulolaitteet" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Sisääntulolaitteet" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Ääniasetukset" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Automaattinen kirkkaus" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Kirkkauden asetukset" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Ota Bluetooth käyttöön" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth-asetukset" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Sulje hätäpuheluikkuna" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "H_ätäyhteystiedot" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Siirry hätäyhteystietojen sivulle" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Siirry takaisin hätäpuhelun yhdistyssivulle" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Tuntematon omistaja" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Hätäyhteystiedot" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Hätäyhteystietoja ei ole saatavilla." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Jotkin sovellukset ovat kiireisiä tai sisältävät tallentamatonta työtä" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Palaute" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Älä häiritse" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Palautteen asetukset" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Käyttäjä:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Toimialue:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Yhdistä" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Takaisin" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Liu'uta ylös avataksesi" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Avaa lukitus" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Tunnistautuminen vaaditaan" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Peru" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Yhdistä" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Tunnistaudu" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Valmiustila" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Lukitse" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Hätä" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Suorita komento" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Ei ilmoituksia" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Ilmoitukset" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Tyhjennä kaikki" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Vahvista:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Sammuta…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Käynnistä uudelleen…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Valmiustila…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Kirjaudu _ulos…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wifi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:73 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi-asetukset" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e. %Bta" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Liitännäistä ei löytynyt" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Liitännäistä '%s' ei voitu ladata." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Wi-Fi-laitetta ei löydy" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi on pois käytöstä" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Ota Wi-Fi käyttöön" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi-yhteyspiste aktiivisena" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Sammuta" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Wi-Fi-yhteyspisteitä ei ole" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Puhelinverkko" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh ja caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Pois" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Päällä" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Caffeine-ajastimet" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "Ei Caffeine-aikavälejä" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Ei aikakatkaisua (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Caffeinen pika-asetukset" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +#| msgid "_Active Duration" +msgid "Caffeine Duration" +msgstr "Caffeine-kesto" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Hallitse Caffeine-kestoa" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Lisää tai poista mukautettuja Caffeine-aikavälejä" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Lisää aikaväli" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Lisää uusi aikaväli" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Lisää" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Pika-asetuksen aikavälit" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Valitse aikaväli" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Oletustyyli" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Tumma tila" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Vaalea tila" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Henkilökohtaiset tiedot" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Syntymäpäivä" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Ensisijainen kieli" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Kotiosoite" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Lääkitystiedot" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Ikä" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Verityyppi" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Pituus" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Paino" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergiat" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Lääkitykset ja terveydentila" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Muut tiedot" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Hätätilatietojen asetukset" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Valmis" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Omistajan nimi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Syntymäpäivä" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Ensisijainen kieli" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Ikä" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Verityyppi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Pituus" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Paino" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Lääkitykset ja terveydentila" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Lisää yhteystieto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Lisää uusi yhteystieto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Yhteyshenkilön nimi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Suhde" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Yhteyshenkilön numero" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Käynnistimiä ei ole määritetty" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Käynnistimet" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Paikannus päällä" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Paikannus pois päältä" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Ei käynnissä olevia mediasoittimia" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Mobiilidata päällä" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Mobiilidata pois" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Yövalo päällä" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Yövalo ei päällä" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodoro-aloitus" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Keskity tehtävääsi %d minuutin ajan" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Ota tauko" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Seuraavaan Pomodoroon on %d minuuttia" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Pomodoro-ajastin" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro pois" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro-pika-asetuksen asetukset" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro-tekniikka" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Aktiivinen kesto" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Keskittymisistunnon kesto" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Tauon kesto" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Istuntojen välillä olevan tauon kesto" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Käynnistä näytön avauduttua" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Käynnistetäänkö ajastin näytön lukituksen avauksen yhteydessä" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Ei näytettäviä asiakirjoja" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Tiketit" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Avaa" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Valitse kansio" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Tikettilaatikon asetukset" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Polut" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Kansion asetukset" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Mistä Phosh etsii tikettejä" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Tikettikansio" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Tänään" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Huomenna" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Ei tapahtumia" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%H.%M" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Koko päivä" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Päättyy" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Nimetön tapahtuma" + +#: plugins/upcoming-events/upcoming-events.c:372 +#, c-format +msgid "No events for the next %d days" +msgstr "Ei tapahtumia seuraavan %d päivän aikana" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Ei tulevia tapahtumia" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Tulevien tapahtumien asetukset" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Päivää" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Päiväväli" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Kuinka monelta päivältä tapahtumat näytetään" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Wi-Fi-yhteyspiste" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:59 +msgid "Turn On" +msgstr "Ota käyttöön" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:76 +msgid "Hotspot On" +msgstr "Yhteyspiste päällä" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:78 +msgid "Hotspot Off" +msgstr "Yhteyspiste pois" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "%u päivän päästä" +#~ msgstr[1] "%u päivän päästä" + +#~ msgid "Add" +#~ msgstr "Lisää" + +#~ msgid "Number" +#~ msgstr "Numero" + +#~ msgid "Scan" +#~ msgstr "Skannaa" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "min" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "t" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "t" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "pv" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "pv" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "kk" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "kk" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "v" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "v" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Sovellus" + +#~ msgid "_Power Off" +#~ msgstr "_Sammuta" + +#~ msgid "_Screenshot" +#~ msgstr "K_uvakaappaus" + +#~ msgid "Unknown application" +#~ msgstr "Tuntematon sovellus" + +#~ msgid "Lock Screen" +#~ msgstr "Lukitse näyttö" + +#~ msgid "Logout" +#~ msgstr "Kirjaudu ulos" + +#, c-format +#~ msgid "On %A" +#~ msgstr "%Ana" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Näytä vain adaptiiviset sovellukset" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d.%m.%y" + +#~ msgid "Unknown artist" +#~ msgstr "Tuntematon artisti" + +#~ msgid "Unknown Song" +#~ msgstr "Tuntematon kappale" diff --git a/po/fr.po b/po/fr.po new file mode 100644 index 000000000..0633f81d7 --- /dev/null +++ b/po/fr.po @@ -0,0 +1,1111 @@ +# French translation for phosh +# Copyright (C) 2018 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Thomas Citharel , 2018 +# Bruno Veilleux , 2018. #zanata +# Frl5 , 2019. #zanata +# Thibault , 2019. #zanata +# Frl5 , 2020. #zanata +# erd9nax0 , 2023. +# Vincent Chatelain , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-03-30 13:03+0000\n" +"PO-Revision-Date: 2025-03-30 15:35+0200\n" +"Last-Translator: Vincent Chatelain \n" +"Language-Team: French \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 45.3\n" +"Plural-Forms: nplurals=2; plural=(n > 1)\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Interface système" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Gestion de fenêtres et lancement d’applications pour mobile" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Cette session vous connecte à Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Paramètre rapide de Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Empêcher la session devenir inactive" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendrier" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Un simple composant calendrier" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Paramètre rapide pour le thème de couleur et le mode sombre" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Basculer le mode sombre" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Informations d’urgence" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Afficher les informations et contacts d’urgence" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Boîte de lanceurs" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Ajouter des lanceurs sur l’écran de verrouillage. Ce greffon est " +"expérimental." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Paramètre rapide des données mobiles" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Basculer les données mobiles" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Paramètre rapide du mode nuit" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Basculer le mode nuit" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Paramètre rapide de Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Minuteur Pomodoro simple" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Boîte de ticket" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Affiche des PDFs sur l’écran de verrouillage. Cette extension est " +"expérimentale." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Prochains évènements" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Afficher les prochains évènements de l’agenda" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Ajouter au dossier" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Créer un nouveau dossier" + +#: src/app-grid-button.c:698 src/app-grid-button.c:754 +msgid "Application" +msgstr "Application" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Afficher toutes les applications" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Afficher seulement les applications adaptées au mobile" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Batterie %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Activé" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Aucun appareil connectable par Bluetooth trouvé" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth désactivé" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Appelant inconnu" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Le réseau Wi-Fi « %s » utilise un portail captif" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Le réseau Wi-Fi utilise un portail captif" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "S’identifier pour le réseau Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Connecté" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Non connecté" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:22 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Valider" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Impossible d’émettre un appel d’urgence" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Erreur interne" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Déconnexion" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s sera automatiquement déconnecté dans %d seconde." +msgstr[1] "%s sera automatiquement déconnecté dans %d secondes." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:69 +msgid "Power Off" +msgstr "Éteindre" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Le système sera automatiquement éteint dans %d seconde." +msgstr[1] "Le système sera automatiquement éteint dans %d secondes." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Redémarrer" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Le système sera automatiquement redémarré dans %d seconde." +msgstr[1] "Le système sera automatiquement redémarré dans %d secondes." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Silencieux" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Muet" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Activé" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Autoriser « %s » à accéder à vos informations de position ?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Géolocalisation" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Oui" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Non" + +#. give visual feedback on error +#: src/lockscreen.c:311 src/ui/lockscreen.ui:280 +msgid "Enter Passcode" +msgstr "Saisir le mot de passe" + +#: src/lockscreen.c:984 +msgid "Checking…" +msgstr "Vérification…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Capture d’écran copiée dans « %s »" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Impossible d’enregistrer la capture d’écran" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:187 +msgid "Screenshot" +msgstr "Capture d’écran" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:527 +msgid "Screenshots" +msgstr "Captures d’écran" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:547 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Capture d’écran de %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:493 src/ui/media-player.ui:211 +msgid "Unknown Title" +msgstr "Titre inconnu" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:501 src/ui/media-player.ui:199 +msgid "Unknown Artist" +msgstr "Artiste inconnu" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Écran intégré" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Inconnu" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "" +"Le type d’authentification du réseau Wi-Fi « %s » n’est pas pris en charge" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Entrer le mot de passe du réseau Wi-Fi « %s »" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Ouvert" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1013 +msgid "Notification" +msgstr "Notification" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "maintenant" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dmin" +msgstr[1] "%dmin" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1j" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dj" +msgstr[1] "%dj" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mois" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "~%dmois" +msgstr[1] "~%dmois" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dan" +msgstr[1] "~%dans" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Plus de %dan" +msgstr[1] "Plus de %dans" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Presque %dan" +msgstr[1] "Presque %dans" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "La fenêtre d’authentification a été fermée par l’utilisateur" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:77 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Mot de passe :" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Désolé, ça n’a pas fonctionné. Veuillez réessayer." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Portrait" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Paysage" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Désactivée" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Activée" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Appuyer sur Échap pour fermer" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Impossible d’exécuter « %s »" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Contrôle du volume de l’interface système" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Les mots de passe ne sont pas identiques." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Le mot de passe ne doit pas être vide" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Torche" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Se souvenir de la décision" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Annuler" + +#: src/ui/app-grid-button.ui:32 +msgid "Remove from _Favorites" +msgstr "Supprimer des _Favoris" + +#: src/ui/app-grid-button.ui:37 +msgid "Add to _Favorites" +msgstr "Ajouter au _Favoris" + +#: src/ui/app-grid-button.ui:42 +msgid "View _Details" +msgstr "Afficher les _détails" + +#: src/ui/app-grid-button.ui:47 +msgid "Uninstall" +msgstr "Désinstaller" + +#: src/ui/app-grid-button.ui:54 +msgid "_Remove from Folder" +msgstr "Supp_rimer du dossier" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Recherche d’applis…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Périphériques de sortie" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Périphériques d’entrée" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Paramètres son" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Activer le Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Paramètres Bluetooth" + +#: src/ui/emergency-menu.ui:24 +msgid "Close the emergency call dialog" +msgstr "Fermer la fenêtre d’appel d’urgence" + +#: src/ui/emergency-menu.ui:50 +msgid "Emergency _Contacts" +msgstr "_Contacts d’urgence" + +#: src/ui/emergency-menu.ui:57 +msgid "Go to the emergency contacts page" +msgstr "Accéder à la page des contacts d’urgence" + +#: src/ui/emergency-menu.ui:80 +msgid "Go back to the emergency dialpad page" +msgstr "Retourner à la page du clavier d’urgence" + +#: src/ui/emergency-menu.ui:103 +msgid "Owner unknown" +msgstr "Propriétaire inconnu" + +#: src/ui/emergency-menu.ui:121 plugins/emergency-info/emergency-info.ui:208 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Contacts d’urgence" + +#: src/ui/emergency-menu.ui:139 +msgid "No emergency contacts available." +msgstr "Aucun contact d’urgence disponible." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "" +"Certaines applications sont en cours d’exécution ou ont des travaux non " +"sauvegardés" + +#: src/ui/gtk-mount-prompt.ui:77 +msgid "User:" +msgstr "Utilisateur :" + +#: src/ui/gtk-mount-prompt.ui:99 +msgid "Domain:" +msgstr "Domaine :" + +#: src/ui/gtk-mount-prompt.ui:131 +msgid "Co_nnect" +msgstr "Co_nnecter" + +#: src/ui/lockscreen.ui:42 src/ui/lockscreen.ui:243 +msgid "Back" +msgstr "Retour" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Glisser vers le haut pour déverrouiller" + +#: src/ui/lockscreen.ui:330 +msgid "Unlock" +msgstr "Déverrouiller" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Authentification requise" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Annuler" + +#: src/ui/network-auth-prompt.ui:54 +msgid "C_onnect" +msgstr "C_onnecter" + +#: src/ui/polkit-auth-prompt.ui:100 +msgid "Authenticate" +msgstr "S’authentifier" + +#: src/ui/power-menu.ui:106 +msgid "Suspend" +msgstr "Suspendre" + +#: src/ui/power-menu.ui:149 +msgid "Lock" +msgstr "Verrouiller" + +#: src/ui/power-menu.ui:225 +msgid "Emergency" +msgstr "Urgences" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Exécuter la commande" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Aucune notification" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Notifications" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Tout effacer" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirmer :" + +#: src/ui/top-panel.ui:31 +msgid "_Power Off…" +msgstr "_Éteindre…" + +#: src/ui/top-panel.ui:58 +msgid "_Restart…" +msgstr "_Redémarrer…" + +#: src/ui/top-panel.ui:85 +msgid "_Suspend…" +msgstr "_Suspendre…" + +#: src/ui/top-panel.ui:112 +msgid "_Log Out…" +msgstr "_Déconnexion…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:86 +msgid "Wi-Fi Settings" +msgstr "Paramètres Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Greffon non trouvé" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Le greffon « %s » n’a pas pu être chargé." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Aucun appareil Wi-Fi trouvé" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi désactivé" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Activer le Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Point d’accès Wi-Fi activé" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Désactiver" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Aucun point d’accès Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Téléphone" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh sous Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Activé" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Désactivé" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Style par défault" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Mode sombre" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Mode clair" + +#: plugins/emergency-info/emergency-info.ui:40 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informations personnelles" + +#: plugins/emergency-info/emergency-info.ui:48 +msgid "Date of Birth" +msgstr "Date de naissance" + +#: plugins/emergency-info/emergency-info.ui:68 +msgid "Preferred Language" +msgstr "Langue préférée" + +#: plugins/emergency-info/emergency-info.ui:88 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Adresse du domicile" + +#: plugins/emergency-info/emergency-info.ui:96 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Information médicale" + +#: plugins/emergency-info/emergency-info.ui:104 +msgid "Age" +msgstr "Âge" + +#: plugins/emergency-info/emergency-info.ui:124 +msgid "Blood Type" +msgstr "Groupe sanguin" + +#: plugins/emergency-info/emergency-info.ui:144 +msgid "Height" +msgstr "Taille" + +#: plugins/emergency-info/emergency-info.ui:164 +msgid "Weight" +msgstr "Poids" + +#: plugins/emergency-info/emergency-info.ui:184 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergies" + +#: plugins/emergency-info/emergency-info.ui:192 +msgid "Medications & Conditions" +msgstr "Médicaments et maladies" + +#: plugins/emergency-info/emergency-info.ui:200 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Autres informations" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Préférences pour les informations d’urgence" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Terminé" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Nom du propriétaire" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Date de naissance" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Langue _préférée" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Â_ge" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "Groupe _sanguin" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Taille" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "P_oids" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Médicaments et maladies" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Ajouter un contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Ajouter un nouveau contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Ajouter" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Nom du _contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relation" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Numéro du _contact" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Aucun lanceur configuré" + +#: plugins/launcher-box/launcher-box.ui:30 +msgid "Launchers" +msgstr "Lanceurs" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Données mobile activées" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Données mobile désactivées" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Mode nuit activé" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Mode nuit désactivé" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Démarrage de Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Se concenter sur votre tâche pendant %d minutes" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Prendre une pause" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Vous disposez de %d avant le prochain Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Minuteur Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro désactivé" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Préférences de paramètre rapide de Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Technique Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Durée d’_activation" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Durée de la session de concentration" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Durée de _pause" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Durée de la pause entre les sessions" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Aucun document à afficher" + +#: plugins/ticket-box/ticket-box.ui:78 +msgid "Tickets" +msgstr "Tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Ouvert" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Choisir un dossier" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Préférences de la boîte à tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Chemins" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Paramètres du dossier" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Emplacement où Phosh recherche vos tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Dossier de tickets" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Aujourd’hui" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Demain" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "Dans %u jour" +msgstr[1] "Dans %u jours" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Aucun évènement" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Journée entière" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Fin à" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Évènement sans titre" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Préférences des prochains évènements" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Jours" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Intervalle de dates" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Nombre de jours pour lequel afficher les évènements" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Échelles du moniteur" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d %%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Point d’accès activé" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Point d’accès désactivé" + +#~ msgid "Add" +#~ msgstr "Ajouter" + +#~ msgid "Number" +#~ msgstr "Numéro" diff --git a/po/fur.po b/po/fur.po new file mode 100644 index 000000000..3e772c910 --- /dev/null +++ b/po/fur.po @@ -0,0 +1,430 @@ +# Friulian translations for phosh. +# Copyright (C) YEAR THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Fabio Tomat , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2021-12-14 10:49+0000\n" +"PO-Revision-Date: 2021-12-22 16:09+0100\n" +"Last-Translator: Fabio Tomat \n" +"Language-Team: Friulian (f.t.public@gmail.com)\n" +"Language: fur\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Gjestion barcons e inviament aplicazions par celulârs" + +#: src/app-grid-button.c:503 +msgid "Application" +msgstr "Aplicazion" + +#: src/app-grid.c:133 +msgid "Show All Apps" +msgstr "Mostre dutis lis app" + +#: src/app-grid.c:136 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostre dome lis app par celulârs" + +#: src/bt-info.c:92 src/feedbackinfo.c:51 src/rotateinfo.c:103 +msgid "On" +msgstr "Ativât" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Leât" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Molât" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Termine session" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s al jessarà in automatic chi di %d secont." +msgstr[1] "%s al jessarà in automatic chi di %d seconts." + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Distude" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Il sisteme si distudarà in automatic chi di %d secont." +msgstr[1] "Il sisteme si distudarà in automatic chi di %d seconts." + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Torne invie" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Il sisteme si tornarà a inviâ in automatic chi di %d secont." +msgstr[1] "Il sisteme si tornarà a inviâ in automatic chi di %d seconts." + +#: src/end-session-dialog.c:269 +msgid "Unknown application" +msgstr "Aplicazion no cognossude" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:44 +msgid "Quiet" +msgstr "Cuiet" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:49 +msgid "Silent" +msgstr "Cidin" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Permeti a '%s' di cognossi la tô posizion?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Posizion" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Sì" + +#: src/location-manager.c:274 +msgid "No" +msgstr "No" + +#: src/lockscreen.c:158 src/ui/lockscreen.ui:265 +msgid "Enter Passcode" +msgstr "Inserìs codiç di sbloc" + +#: src/lockscreen.c:341 +msgid "Checking…" +msgstr "Verifiche…" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:419 +msgid "%A, %B %-e" +msgstr "%A, %d di %B" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:179 +msgid "Unknown Title" +msgstr "Titul no cognossût" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:162 +msgid "Unknown Artist" +msgstr "Artist no cognossût" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Display integrât" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "No cognossût" + +#: src/network-auth-prompt.c:187 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Gjenar di autenticazion de rêt wifi “%s” no supuartât" + +#: src/network-auth-prompt.c:192 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Inserìs la password pe rêt wifi “%s”" + +#: src/notifications/mount-notification.c:122 +msgid "Open" +msgstr "Vierç" + +#: src/notifications/notification.c:383 src/notifications/notification.c:639 +msgid "Notification" +msgstr "Notifiche" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "o" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "o" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "mês" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mês" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "a" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "a" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "cumò" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Plui di %da" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Cuasi %da" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:228 +msgid "Authentication dialog was dismissed by the user" +msgstr "Dialic di autenticazion refudât dal utent" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:83 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Password:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Mi displâs, no je lade drete. Prove di gnûf." + +#: src/rotateinfo.c:81 +msgid "Portrait" +msgstr "Verticâl" + +#: src/rotateinfo.c:84 +msgid "Landscape" +msgstr "Orizontâl" + +#. Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) +#. Translators: Automatic screen orientation is off (locked/disabled) +#: src/rotateinfo.c:103 src/rotateinfo.c:186 +msgid "Off" +msgstr "Disativât" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Lis password no corispuindin." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "La password no pues jessi vueide" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Torce" + +#: src/ui/app-auth-prompt.ui:40 +msgid "Remember decision" +msgstr "Visâsi de decision" + +#: src/ui/app-auth-prompt.ui:53 src/ui/end-session-dialog.ui:53 +msgid "Cancel" +msgstr "Anule" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Va ben" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "Aplicazion" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "Gjave dai _preferîts" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "Zonte tai _preferîts" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Cîr aplicazions…" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Cualchi aplicazion e je impegnade opûr e à lavôrs no salvâts" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Utent:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Domini:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "Co_net" + +#: src/ui/lockscreen.ui:42 +msgid "Slide up to unlock" +msgstr "Scor in sù par sblocâ" + +#: src/ui/lockscreen.ui:309 +msgid "Emergency" +msgstr "Emergjence" + +#: src/ui/lockscreen.ui:325 +msgid "Unlock" +msgstr "Sbloche" + +#: src/ui/lockscreen.ui:365 +msgid "Back" +msgstr "Indaûr" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Autenticazion necessarie" + +#: src/ui/network-auth-prompt.ui:41 +msgid "_Cancel" +msgstr "_Anule" + +#: src/ui/network-auth-prompt.ui:59 +msgid "C_onnect" +msgstr "C_onet" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Autentiche" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Eseguìs comant" + +#: src/ui/run-command-dialog.ui:26 +msgid "Press ESC to close" +msgstr "Frache Esc par sierâ" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Conferme:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Bloche schermi" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Jes" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:172 +msgid "Cellular" +msgstr "Cellulâr" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d.%m.%y" diff --git a/po/fy.po b/po/fy.po new file mode 100644 index 000000000..981960d91 --- /dev/null +++ b/po/fy.po @@ -0,0 +1,772 @@ +# Frisian translation for phosh. +# Copyright (C) 2023 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# FIRST AUTHOR , YEAR. +# Vancha March , 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-09-06 10:56+0000\n" +"PO-Revision-Date: 2023-09-08 12:52+0200\n" +"Last-Translator: Vancha March \n" +"Language-Team: Western Frisian \n" +"Language: fy\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1)\n" +"X-DL-Team: fy\n" +"X-DL-Module: phosh\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-State: None\n" +"X-Generator: Gtranslator 42.0\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Kalinder" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "In simpele kalinder widget" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Aplikaasje" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Alle applikaasjes sjen litte" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/call-notification.c:61 +msgid "Unknown caller" +msgstr "" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "" + +#: src/end-session-dialog.c:163 +msgid "Log Out" +msgstr "Utlogge" + +#: src/end-session-dialog.c:166 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" + +#: src/end-session-dialog.c:172 +msgid "Power Off" +msgstr "" + +#: src/end-session-dialog.c:173 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" + +#: src/end-session-dialog.c:179 +msgid "Restart" +msgstr "Werstarte" + +#: src/end-session-dialog.c:180 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" + +#: src/end-session-dialog.c:270 +msgid "Unknown application" +msgstr "Unbekende applikaasje" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Nee" + +#: src/lockscreen.c:174 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "Tagongskoade ynfiere" + +#: src/lockscreen.c:397 +msgid "Checking…" +msgstr "Oan it kontrolearjen…" + +#: src/screenshot-manager.c:213 +msgid "Screenshot" +msgstr "Skermôfbylding" + +#: src/screenshot-manager.c:214 +msgid "Screenshot copied to clipboard" +msgstr "Skermôfbylding nei klemboerd kopiearre" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:321 src/ui/media-player.ui:161 +msgid "Unknown Title" +msgstr "Unbekende Titel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:329 src/ui/media-player.ui:148 +msgid "Unknown Artist" +msgstr "Unbekende Artyst" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Ynboud skerm" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Unbekend" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Autentikaasje type fan it wifi network “%s” wurd net stipe" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Iepenje" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "Notifikaasje" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "no krekt" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Oer %dy" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Hast %dy" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Wachtwurd:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Sorry, dat wurke net. Probearje it noch ris." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Ut" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Oan" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Druk op ESC om te slúten" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Koe '%s' net útfiere" + +#: src/settings/audio-settings.c:373 +msgid "Phone Shell Volume Control" +msgstr "" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "De wachtwurden komme net oerien." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Wachtwurd mei net leech wêze" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lampe" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Beslut ûnthalde" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +msgid "Cancel" +msgstr "Ofbrekke" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Applikaasje" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "_Details besjen" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Applikaasjes sykje…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Utfier apparaten" + +#: src/ui/audio-settings.ui:107 +msgid "Input Devices" +msgstr "Ynfier apparaten" + +#: src/ui/audio-settings.ui:134 +msgid "Sound Settings" +msgstr "Lûdsynstellings" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +msgid "Emergency Contacts" +msgstr "" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Brûker:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "Werom" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "" + +#: src/ui/power-menu.ui:69 +msgid "_Power Off" +msgstr "" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "_Skermôfbylding" + +#: src/ui/power-menu.ui:192 +msgid "_Emergency" +msgstr "" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Kommando útfiere" + +#: src/ui/settings.ui:296 +msgid "No notifications" +msgstr "Gjin notifikaasjes" + +#: src/ui/settings.ui:336 +msgid "Clear all" +msgstr "" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +msgid "%A, %B %-e" +msgstr "" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Persoanlike ynformaasje" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Foarkars taal" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "Medyske ynformaasje" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "Lingte" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "Gewicht" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "Allergyen" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "Oare ynformaasje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Klear" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "Kontakt tafoegje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "Nij kontakt tafoegje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "Tafoegje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:315 +msgid "New Contact Name" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:329 +msgid "Relationship" +msgstr "Relaasje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:342 +msgid "Number" +msgstr "Nûmer" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Gjin dokuminten om sjen te litten" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hjoed" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Moarn" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "yn %d dei" +msgstr[1] "Yn %d dagen" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Gjin eveneminten" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "De folsleine dei" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Namleas evenemint" diff --git a/po/gl.po b/po/gl.po new file mode 100644 index 000000000..45565a484 --- /dev/null +++ b/po/gl.po @@ -0,0 +1,782 @@ +# Galician translation for phosh. +# Copyright (C) 2022 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# FIRST AUTHOR , YEAR. +# Fran Diéguez , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-07-05 08:48+0000\n" +"PO-Revision-Date: 2023-08-16 02:58+0200\n" +"Last-Translator: Fran Diéguez \n" +"Language-Team: Galician \n" +"Language: gl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-Module: phosh\n" +"X-DL-State: Translating\n" +"X-DL-Team: gl\n" +"X-Generator: Poedit 3.3.2\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Shell do móvil" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Xestión de xanelas e inicio de aplicacións para o móvil" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Esta sesión arrinca en Phosh" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendario" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Un trebello simple do calendario" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Caixa de billetes" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Mostrar PDFs na pantalla de bloqueo.Este engadido é experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Vindeiros eventos" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Mostrar os eventos vindeiros do calendario" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Aplicación" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Mostrar todas as aplicacións" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostrar só as aplicacións amigábeis co móvil" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Activado" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/call-notification.c:60 +msgid "Unknown caller" +msgstr "Número descoñecido" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Ancorado" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desancorado" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "Aceptar" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Non foi posíbel realizar unha chamada de emerxencia" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Erro interno" + +#: src/end-session-dialog.c:163 +msgid "Log Out" +msgstr "Saír da sesión" + +#: src/end-session-dialog.c:166 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s pechará a sesión automaticamente en %d segundo." +msgstr[1] "%s pechará a sesión automaticamente en %d segundos." + +#: src/end-session-dialog.c:172 +msgid "Power Off" +msgstr "Apagar" + +#: src/end-session-dialog.c:173 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "O sistema apagarase automaticamente en %d segundo." +msgstr[1] "O sistema apagarase automaticamente en %d segundos." + +#: src/end-session-dialog.c:179 +msgid "Restart" +msgstr "Reiniciar" + +#: src/end-session-dialog.c:180 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "O sistema reiniciarase automaticamente en %d segundo." +msgstr[1] "O sistema reiniciarase automaticamente en %d segundos." + +#: src/end-session-dialog.c:270 +msgid "Unknown application" +msgstr "Aplicación descoñecida" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Calado" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silenciado" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Activado" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Permitirlle a «%s» acceder á información da localización?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Xeolocalización" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Sí" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Non" + +#: src/lockscreen.c:174 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "Escribir código de paso" + +#: src/lockscreen.c:397 +msgid "Checking…" +msgstr "Comprobando…" + +#: src/screenshot-manager.c:212 +msgid "Screenshot" +msgstr "Captura de pantalla" + +#: src/screenshot-manager.c:213 +msgid "Screenshot copied to clipboard" +msgstr "Captura de pantalla copiada no portapapeis" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:321 src/ui/media-player.ui:161 +msgid "Unknown Title" +msgstr "Título descoñecido" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:329 src/ui/media-player.ui:148 +msgid "Unknown Artist" +msgstr "Artista descoñecido" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Pantalla embebida" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Descoñecido" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "O tipo de autenticación do rede wifi «%s» non é compatíbel" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Escriba o contrasinal para a rede wifi «%s»" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Abrir" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "Notificación" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "h" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "h" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "mo" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mos" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "a" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "a" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "agora" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Máis de %dy" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Case %dy" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "O diálogo de autenticación foi rexeitada polo usuario" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Contrasinal:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Desculpe, iso non funcionou. Ténteo de novo." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Retrato" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Apaisado" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desactivada" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Activado" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Prema ESC para pechar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Fallou a execución de «%s»" + +#: src/settings/audio-settings.c:373 +msgid "Phone Shell Volume Control" +msgstr "Control de volume do shell do teléfono" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Os contrasinais non coinciden." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "O contrasinal non pode estar baleira" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lanterna" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Lembrar decisión" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +msgid "Cancel" +msgstr "Cancelar" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Aplicación" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Quitar dos _favoritos" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Engadir aos _favoritos" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Ver _detalles" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Buscar aplicacións…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Dispositivos de saída" + +#: src/ui/audio-settings.ui:107 +msgid "Input Devices" +msgstr "Dispositivos de entrada" + +#: src/ui/audio-settings.ui:134 +msgid "Sound Settings" +msgstr "Preferencias de son" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Pechar o diálogo de chamadas de emerxencia" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Contactos de _emerxencia" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Vaia á páxina de contactos de emerxencia" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Volva á páxina de marcación de emerxencia" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Propietario descoñecido" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +msgid "Emergency Contacts" +msgstr "Contactos de emerxencia" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Non hai contactos de emerxencia dispoñíbeis." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Algunha aplicación está ocupada ou ten traballo sen gardar" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Usuario:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Dominio:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "Co_nectar" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "Atrás" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "Deslizar arriba para desbloquear" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "Desbloquear" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Requírese autenticación" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Cancelar" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "C_onectar" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Autenticar" + +#: src/ui/power-menu.ui:69 +msgid "_Power Off" +msgstr "_Apagar" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "_Bloquear" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "_Captura de pantalla" + +#: src/ui/power-menu.ui:192 +msgid "_Emergency" +msgstr "_Emerxencia" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Executar orde" + +#: src/ui/settings.ui:296 +msgid "No notifications" +msgstr "Non hai notificacións" + +#: src/ui/settings.ui:336 +msgid "Clear all" +msgstr "Limpar todo" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Confirmar:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "_Apagar…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "_Reiniciar…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspender…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Saír da sesión…" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Engadido non atopado" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Non se puido cargar o engadido «%s»." + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wifi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Celular" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Información persoal" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Data de nacemento" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Linguaxe preferida" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Enderezo da casa" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "Información médica" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "Idade" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "Grupo sanguíneo" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "Altura" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "Peso" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "Alerxias" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Medicación e condicións" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "Outra información" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Preferencias de información de emerxencia" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Feito" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Nome do propietario" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "Medicación e condiciónss" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "Engadir contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "Engadir novo contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "Engadir" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:315 +msgid "New Contact Name" +msgstr "Novo nome de contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:329 +msgid "Relationship" +msgstr "Relación" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:342 +msgid "Number" +msgstr "Número" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Non hai documentos a mostrar" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Entradas" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Escoller cartafol" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Abrir" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferencias da caixa de billetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Rutas" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Preferncias do cartafol" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Onde Phosh busca as súas entradas" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Cartafol de entradas" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hoxe" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Mañá" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "En %d día" +msgstr[1] "En %d días" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Sen eventos" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Todo o día" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Remata" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Evento sen título" + +#~ msgid "Emergency" +#~ msgstr "Emerxencia" + +#~ msgid "Lock Screen" +#~ msgstr "Bloquear pantalla" + +#~ msgid "Logout" +#~ msgstr "Saír da sesión" diff --git a/po/he.po b/po/he.po new file mode 100644 index 000000000..39517762a --- /dev/null +++ b/po/he.po @@ -0,0 +1,1338 @@ +# son of dirt , 2019. #zanata +# Yosef Or Boczko , 2022-2024. +# +# SPDX-FileCopyrightText: 2024 Yaron Shahrabani +# Yaron Shahrabani , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-11-26 07:54+0000\n" +"PO-Revision-Date: 2025-11-27 14:47+0200\n" +"Last-Translator: Yaron Shahrabani \n" +"Language-Team: Hebrew \n" +"Language: he\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: ‪nplurals=4; plural=(n == 1) ? 0 : ((n == 2) ? 1 : ((n > 10 && " +"n % 10 == 0) ? 2 : 3));\n" +"X-Generator: Gtranslator 49.0\n" +"X-DL-VCS-Web: https://gitlab.gnome.org/World/Phosh/phosh\n" +"X-DL-Lang: he\n" +"X-DL-Module: phosh\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-State: None\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "מעטפת לטלפון" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "מנהל חלונות ומשגר יישומים לטלפון נייד" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "הפעלה זאת מחברת אותך ל־Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "הגדרות קפאין מהירות" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "למנוע מעבר למצב לא פעיל של ההפעלה" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "לוח שנה" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "יישומון לוח שנה פשוט" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "הגדרה מהירה של ערכת צבעים / מצב כהה" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "שינוי מצב כהה" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "פרטי חירום" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "להציג פרטי חירום וקשר" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "קופסת משגר" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "הוספת משגרים למסך הנעילה. זה תוסף נסיוני." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "הגדרות מיקום מהירות" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "כיבוי/הפעלת שירותי מיקום" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "נגני מדיה" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "מעקב אחר נגני המדיה שפעילים כרגע" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "הגדרת נתונים ניידים מהירה" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "כיבוי/הפעלת נתונים ניידים" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "הגדרות מהירות לתאורת לילה" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "כיבוי/הפעלת תאורת לילה" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "הגדרות פומודורו מהירות" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "מתזמן פומודורו פשוט" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "תיבת כרטיסים" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "הצגת קובצי PDF במסך הנעילה. זהו תוסף ניסיוני." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "אירועים קרובים" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "הצגת אירועים קרובים בלוח השנה" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "הוספה לתיקייה" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "יצירת תיקייה חדשה" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "יישום" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "הצגת כל היישומים" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "הצגת יישומים מותאמים לנייד בלבד" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "סוללה %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "בלוטות'" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "פעיל" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "לא נמצאו התקני בלוטות׳ שאפשר להתחבר אליהם" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "בלוטות׳ מושבת" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "שיחה לא מזוהה" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "הרשת האלחוטית ‚%s’ משתמשת במסך חסימה" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "הרשת האלחוטית משתמשת במסך חסימה" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "כניסה לרשת אלחוטית" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "מעוגן" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "לא מעוגן" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "אישור" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "לא ניתן לבצע שיחת חירום" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "שגיאה פנימית" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "יציאה" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "המשתמש %s יצא אוטומטית בעוד שנייה." +msgstr[1] "המשתמש %s יצא אוטומטית בעוד שתי שניות." +msgstr[2] "המשתמש %s יצא אוטומטית בעוד %d שניות." +msgstr[3] "‫המשתמש %s יצא אוטומטית בעוד %d שניות." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "כיבוי" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "המערכת תכבה אוטומטית בעוד שנייה." +msgstr[1] "המערכת תכבה אוטומטית בעוד שתי שניות." +msgstr[2] "המערכת תכבה אוטומטית בעוד %d שניות." +msgstr[3] "‫המערכת תכבה אוטומטית בעוד %d שניות." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "הפעלה מחדש" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "המערכת תופעל אוטומטית מחדש בעוד שנייה." +msgstr[1] "‫המערכת תופעל אוטומטית מחדש בעוד שתי שניות." +msgstr[2] "המערכת תופעל אוטומטית מחדש בעוד %d שניות." +msgstr[3] "‫המערכת תופעל אוטומטית מחדש בעוד %d שניות." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "רטט" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "שקט" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "פעיל" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "לאפשר ל־„%s” לגשת למידע על המיקום שלך?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "מיקום גאוגרפי" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "כן" + +#: src/location-manager.c:272 +msgid "No" +msgstr "לא" + +#. give visual feedback on error +#: src/lockscreen.c:313 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "הזנת ססמה" + +#: src/lockscreen.c:956 +msgid "Checking…" +msgstr "בודק…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "צילום המסך נשמר אל ‚%s’" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "שמירת צילום המסך נכשלה" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "צילום מסך" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "צילומי מסך" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "צילום מסך מ־%s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "כותרת לא ידועה" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "אמן לא ידוע" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "תצוגה מובנית" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "‏%s ‏%s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "‏%s ‏%s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "לא ידוע" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "אימות רשת אלחוטית מסוג „%s” אינו נתמך" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "נא להזין את הססמה עבור רשת האינטרנט האלחוטית „%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "פתיחה" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "התרעה" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "לא ידוע1" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "< 30 שנ׳" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "< דק׳" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "בערך דקה" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "דקה" +msgstr[1] "שתי דק׳" +msgstr[2] "‫%d דק׳" +msgstr[3] "‫%d דק׳" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "בערך שעה" +msgstr[1] "בערך שעתיים" +msgstr[2] "בערך %d שעות" +msgstr[3] "בערך %d שעות" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "בערך יום" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "יום" +msgstr[1] "הוספהיומיים" +msgstr[2] "‫%d יום" +msgstr[3] "‫%d ימים" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "בערך חודש" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "חודש" +msgstr[1] "חודשיים" +msgstr[2] "‫%d חודשים" +msgstr[3] "‫%d חודשים" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "בערך שנה" +msgstr[1] "בערך שנתיים" +msgstr[2] "בערך %d שנה" +msgstr[3] "בערך %d שנים" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "יותר משנה" +msgstr[1] "יותר משנתיים" +msgstr[2] "יותר מ־%d שנה" +msgstr[3] "יותר מ־%d שנים" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "כמעט שנה" +msgstr[1] "כמעט שנתיים" +msgstr[2] "כמעט %d שנה" +msgstr[3] "כמעט %d שנים" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "המשתמש בחר להתעלם מתיבת דו־שיח האימות" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "ססמה:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "לצערינו פעולה זו לא צלחה. יש לנסות שוב." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "לאורך" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "לרוחב" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "כבוי" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "פעיל" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "יש להקיש Esc לסגירה" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "הרצת „%s” נכשלה" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "בקרת עצמת שמע של מעטפת הטלפון" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "הססמאות אינן תואמות." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "הססמה אינה יכולה להישאר ריקה" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "פנס" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "לזכור את הבחירה" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "ביטול" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "הסרה מה_מועדפים" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "הוספה למועד_פים" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "הצגת _פרטים" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "הסרה" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "הסרה מ_תיקייה" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "חיפוש יישומים…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "התקני פלט" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "התקני קלט" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "הגדרות שמע" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "בהירות אוטומטית" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "הגדרות בהירות" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "הפעלת בלוטות׳" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "הגדרות בלוטות׳" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "סגירת דו־שיח לחיוג במקרה חירום" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_אנשי קשר למקרה חירום" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "מעבר לעמוד אנשי קשר למקרה חירום" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "חזרה לדף החיוג במקרה חירום" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "בעלים לא ידוע" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "אנשי קשר למקרה חירום" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "אין בנמצא אנשי קשר למקרה חירום." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "כמה יישומים עסוקים או שיש עבודה שלא נשמרה" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "משוב" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "לא להפריע" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "הגדרות משוב" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "משתמש:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "מתחם:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "הת_חברות" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "חזרה" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "החלקה כלפי מעלה לביטול הנעילה" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "שחרור" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "נדרש אימות" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_ביטול" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "הת_חברות" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "אימות" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "השהיה" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "נעילה" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "מצב חירום" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "הרצת פקודה" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "אין התרעות" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "התראות" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "ניקוי הכול" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "אישור:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_כיבוי…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "ה_פעלה מחדש…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_השהיה…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "י_ציאה…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "רשת אלחוטית" + +#: src/ui/wifi-status-page.ui:89 +msgid "Wi-Fi Settings" +msgstr "הגדרות רשת אלחוטית" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "לא נמצאו תוספים" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "לא ניתן לטעון את התוסף '%s'." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "לא נמצא התקן לרשת אלחוטית" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "הרשת האלחוטית מושבתת" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "הפעלת רשת אלחוטית" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "נקודת גישה אלחוטית פעילה" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "כיבוי" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "אין נק׳ גישה לרשת אלחוטית" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "רשת סלולרית" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "‫Phosh על קפאין" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "כבוי" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "פעיל" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "מתזמני קפאין" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "אין הפרשי זמן בין קפאין" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "אין תום זמן המתנה (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "הגדרות קפאין מהירות" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "משך קפאין" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "ניהול משך זמן קפאין" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "הוספת או הסרת משכי זמן מותאמים של קפאין" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "הוספת משך זמן" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "הוספת משך זמן חדש" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "הו_ספה" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "משכי זמן להתחלה מהירה" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 דק׳" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 דק׳" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 דק׳" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "שעה" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "בחירת משך זמן" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "סגנון ברירת מחדל" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "מצב כהה" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "מצב בהיר" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "מידע אישי" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "תאריך לידה" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "שפה מועדפת" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "כתובת בית" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "מידע רפואי" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "גיל" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "סוג דם" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "גובה" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "משקל" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "אלרגיות" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "תרופות והגבלות" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "מידע נוסף" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "העדפות מידע למקרה חירום" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "בוצע" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "שם _בעלים" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "תאריך _לידה" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_שפה מועדפת" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_גיל" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "סוג _דם" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_גובה" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "מ_שקל" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "תרופות והגבלות" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "הוספת איש קשר" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "הוספת איש קשר חדש" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_שם איש קשר" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "קרבה" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "מ_ספר איש קשר" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "לא הוגדרו משגרים" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "משגרים" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "מיקום פעיל" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "מיקום כבוי" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "אין נגני מדיה פעילים" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "נתונים ניידים פועלים" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "נתונים ניידים כבויים" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "תאורת לילה פעילה" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "תאורת לילה כבויה" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "התחלת פומודורו" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "נא להתמקד על המשימה שלך למשך %d דקות" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "נא לקחת הפסקה" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "נותרו לך %d דקות עד הפומודורו הבא" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "מתזמן פומודורו" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "פומודורו כבוי" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "העדפות הגדרות מהירות לפומודורו" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "טכניקת פומודורו" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "משך _פעילות" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "משך פעילות ממוקדת" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "משך הפ_סקה" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "משך ההפסקה בין הפעילויות" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "להפעיל ל_אחר שחרור נעילה" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "האם להתחיל את המתזמן עם שחרור נעילת המסך" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "אין מסמכים להצגה" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "תעודות" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_פתיחה" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "בחירת תיקייה" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "העדפות תיבת כרטיסים" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "נתיבים" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "הגדרות תיקיות" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "היכן Phosh יחפש את הכרטיסים שלך" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "תיקיית כרטיסים" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "היום" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "מחר" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "אין אירועים" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "‏‎%l:%M ‏‏%p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "כל היום" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "סיום" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "אירוע ללא כותרת" + +#: plugins/upcoming-events/upcoming-events.c:372 +#, c-format +msgid "No events for the next %d days" +msgstr "אין אירועים למשך %d הימים הקרובים" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "אין אירועים קרובים" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "העדפות אירועים קרובים" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "ימים" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "טווח תאריכים" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "מספר הימים להצגת אירועים" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "קני מידה של צגים" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:75 +msgid "Hotspot On" +msgstr "נק׳ חמה פעילה" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:77 +msgid "Hotspot Off" +msgstr "נק׳ חמה כבויה" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "מחר" +#~ msgstr[1] "מחרתיים" +#~ msgstr[2] "בעוד %u ימים" +#~ msgstr[3] "בעוד %u ימים" + +#~ msgid "Add" +#~ msgstr "הוספה" + +#~ msgid "Number" +#~ msgstr "מספר" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "המסך צולם והועתק ללוח הגזירים" + +#~ msgid "Scan" +#~ msgstr "סריקה" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "שנ" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "דק" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "דק" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "ש" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "ש" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "ימ" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "ימ" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "חדש" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "חדש" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "שנ" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "שנ" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "יישום" + +#~ msgid "_Power Off" +#~ msgstr "_כיבוי" + +#~ msgid "_Screenshot" +#~ msgstr "צי_לום מסך" + +#~ msgid "_Emergency" +#~ msgstr "מצב _חירום" + +#~ msgid "Unknown application" +#~ msgstr "יישום לא ידוע" + +#~ msgid "Lock Screen" +#~ msgstr "מסך נעילה" + +#~ msgid "Logout" +#~ msgstr "יציאה" diff --git a/po/hi.po b/po/hi.po new file mode 100644 index 000000000..a314b4acb --- /dev/null +++ b/po/hi.po @@ -0,0 +1,951 @@ +# Hindi translation for phosh. +# Copyright (C) 2023 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Translators: +# Hemish , 2023. +# Scrambled777 , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2024-08-06 08:05+0000\n" +"PO-Revision-Date: 2024-08-07 17:21+0530\n" +"Last-Translator: Scrambled777 \n" +"Language-Team: Hindi \n" +"Language: hi\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Gtranslator 46.1\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "फ़ॉश" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "फोन शैल" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "मोबाइल के लिए विंडो प्रबंधन और अनुप्रयोग लॉंचिंग" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "यह सत्र आपको फ़ॉश में ले जाता है" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "कैफीन त्वरित सेटिंग" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "सत्र को निष्क्रिय होने से रोकें" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "पंचांग" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "एक साधारण कैलेंडर विजेट" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "गहरा मोड / रंग योजना त्वरित सेटिंग" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "गहरा मोड टॉगल करें" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "आपातकालीन जानकारी" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "आपातकालीन जानकारी और संपर्क दिखाएं" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "प्रक्षेपक बॉक्स" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "लॉक स्क्रीन पर लॉन्चर जोड़ें। यह प्लगइन प्रायोगिक है।" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "मोबाइल डेटा त्वरित सेटिंग" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "मोबाइल डेटा चालू/बंद करें" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "रात्रि प्रकाश त्वरित सेटिंग" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "रात्रि प्रकाश को चालू/बंद करें" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "टिकट बक्सा" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "लॉक स्क्रीन पर पीडीएफ दिखाएं। यह प्लगिन प्रायोगिक है।" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "आगामी कार्यक्रम" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "आगामी पंचांग कार्यक्रम दिखाएं" + +#: src/app-grid-button.c:153 +msgid "Add to Folder" +msgstr "फोल्डर में जोड़ें" + +#: src/app-grid-button.c:177 +msgid "Create new folder" +msgstr "नया फोल्डर बनाएं" + +#: src/app-grid-button.c:692 src/app-grid-button.c:749 +msgid "Application" +msgstr "अनुप्रयोग" + +#: src/app-grid.c:263 +msgid "Show All Apps" +msgstr "सभी ऐप्स दिखाएं" + +#: src/app-grid.c:266 +msgid "Show Only Mobile Friendly Apps" +msgstr "केवल मोबाइल मैत्रीपूर्ण ऐप्स दिखाएं" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:72 +#, c-format +msgid "Battery %.0f%%" +msgstr "बैटरी %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "ब्लूटूथ" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "सक्रिय" + +#: src/bt-status-page.c:86 +msgid "No connectable Bluetooth Devices found" +msgstr "कनेक्ट करने योग्य कोई ब्लूटूथ उपकरण नहीं मिला" + +#: src/bt-status-page.c:90 +msgid "Bluetooth disabled" +msgstr "ब्लूटूथ अक्षम" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "अज्ञात कॉलर" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "डॉक किया गया" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "अनडॉक किया गया" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "ठीक है" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "आपातकालीन कॉल करने में असमर्थ" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "आंतरिक त्रुटि" + +#: src/end-session-dialog.c:174 +msgid "Log Out" +msgstr "लॉग आउट" + +#: src/end-session-dialog.c:177 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s को %d सेकेंड में स्वतः लॉग आउट कर दिया जाएगा।" +msgstr[1] "%s को %d सेकेंडों में स्वतः लॉग आउट कर दिया जाएगा।" + +#: src/end-session-dialog.c:183 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "बंद करें" + +#: src/end-session-dialog.c:184 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "यह सिस्टम %d सेकेंड में स्वतः बंद हो जाएगा।" +msgstr[1] "यह सिस्टम %d सेकेंडों में स्वतः बंद हो जाएगा।" + +#: src/end-session-dialog.c:190 +msgid "Restart" +msgstr "फिर आरंभ करें" + +#: src/end-session-dialog.c:191 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "यह सिस्टम %d सेकेंड में स्वतः पुन: आरंभ हो जाएगा।" +msgstr[1] "यह सिस्टम %d सेकेंडों में स्वतः पुन: आरंभ हो जाएगा।" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "शांत" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "मूक" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "सक्रिय" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "'%s' को अपनी स्थानीय जानकारी पता करने की अनुमति दें?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "भूस्थिति" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "हाँ" + +#: src/location-manager.c:274 +msgid "No" +msgstr "नहीं" + +#: src/lockscreen.c:166 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "पासकोड दर्ज करें" + +#: src/lockscreen.c:392 +msgid "Checking…" +msgstr "जांच हो रही है…" + +#: src/screenshot-manager.c:221 src/ui/power-menu.ui:192 +msgid "Screenshot" +msgstr "स्क्रीनशॉट" + +#: src/screenshot-manager.c:222 +msgid "Screenshot copied to clipboard" +msgstr "स्क्रीनशॉट को क्लिपबोर्ड पर कॉपी किया गया" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:446 src/ui/media-player.ui:213 +msgid "Unknown Title" +msgstr "अज्ञात शीर्षक" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:454 src/ui/media-player.ui:201 +msgid "Unknown Artist" +msgstr "अज्ञात कलाकार" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "अंतर्निर्मित प्रदर्शन" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "अज्ञात" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "वाई-फाई नेटवर्क का प्रमाणीकरण प्रकार “%s” समर्थित नहीं है" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "वाई-फाई नेटवर्क “%s” के लिए पासवर्ड दर्ज करें" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "खोलें" + +#: src/notifications/notification.c:405 src/notifications/notification.c:678 +msgid "Notification" +msgstr "अधिसूचना" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:86 +msgid "now" +msgstr "अभी" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:91 +#, c-format +msgid "<30s" +msgstr "<30 सेकंड" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:96 +#, c-format +msgid "<1m" +msgstr "<1 मिनट" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:101 +#, c-format +msgid "~1m" +msgstr "~1 मिनट" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:109 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d मिनट" +msgstr[1] "%d मिनट" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:117 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d घंटे" +msgstr[1] "~%d घंटे" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:122 +#, c-format +msgid "~1d" +msgstr "~1 दिन" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:127 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d दिन" +msgstr[1] "%d दिन" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:132 +#, c-format +msgid "~1mo" +msgstr "~1 महीना" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:137 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d महीना" +msgstr[1] "%d महीने" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:147 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d वर्ष" +msgstr[1] "~%d वर्षों" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:151 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%d वर्ष से ज़्यादा" +msgstr[1] "%d वर्षों से ज़्यादा" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:156 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "लगभग %d वर्ष" +msgstr[1] "लगभग %d वर्षों" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "सत्यापन संवाद को उपयोक्ता के द्वारा खारिज कर दिया गया" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "पासवर्ड:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "माफ करें, वह काम नहीं किया। कृपया दोबारा कोशिश करें।" + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "पोर्ट्रेट" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "लैंडस्केप" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "अक्षम" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "सक्षम" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "बंद करने के लिए ESC दबाएं" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "'%s' को चलाना विफल हुआ" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "फोन शैल आवाज़ नियंत्रण" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "पासवर्ड मेल नहीं खाते।" + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "पासवर्ड खाली नहीं हो सकता है" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "टॉर्च" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "निर्णय याद रखें" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:293 +msgid "Cancel" +msgstr "रद्द करें" + +#: src/ui/app-grid-button.ui:23 +msgid "Remove from _Favorites" +msgstr "पसंदीदा से हटाएं (_F)" + +#: src/ui/app-grid-button.ui:28 +msgid "Add to _Favorites" +msgstr "पसंदीदा में जोड़े (_F)" + +#: src/ui/app-grid-button.ui:33 +msgid "View _Details" +msgstr "विवरण देखें (_D)" + +#: src/ui/app-grid-button.ui:41 +msgid "_Remove from Folder" +msgstr "फोल्डर से हटाएं (_R)" + +#: src/ui/app-grid.ui:24 +msgid "Search apps…" +msgstr "ऐप्स खोजें…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "निर्गत उपकरण" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "आगत यंत्र" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "ध्वनि सेटिंग्स" + +#: src/ui/bt-status-page.ui:32 +msgid "Enable Bluetooth" +msgstr "ब्लूटूथ सक्षम करें" + +#: src/ui/bt-status-page.ui:53 +msgid "Bluetooth Settings" +msgstr "ब्लूटूथ सेटिंग" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "आपातकालीन कॉल संवाद बंद करें" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "आपातकालीन संपर्क (_C)" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "आपातकालीन संपर्क पृष्ठ पर जाएं" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "आपातकालीन डायलपैड पृष्ठ पर वापस जाएं" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "मालिक अज्ञात" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:234 +msgid "Emergency Contacts" +msgstr "आपातकालीन संपर्क" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "कोई आपातकालीन संपर्क उपलब्ध नहीं है।" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "कुछ अनुप्रयोग व्यस्त हैं या उनमें सहेजा न गया काम है" + +#: src/ui/gtk-mount-prompt.ui:88 +msgid "User:" +msgstr "उपयोक्ता:" + +#: src/ui/gtk-mount-prompt.ui:111 +msgid "Domain:" +msgstr "डोमेन:" + +#: src/ui/gtk-mount-prompt.ui:144 +msgid "Co_nnect" +msgstr "जुड़ें (_n)" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "पीछे" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "खोलने के लिए ऊपर स्लाइड करें" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "खोलें" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "सत्यापन आवश्यक" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "रद्द करें (_C)" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "जुड़ें (_o)" + +#: src/ui/polkit-auth-prompt.ui:117 +msgid "Authenticate" +msgstr "सत्यापित करें" + +#: src/ui/power-menu.ui:109 +msgid "Suspend" +msgstr "निलंबित करें" + +#: src/ui/power-menu.ui:153 +msgid "Lock" +msgstr "ताला लगायें" + +#: src/ui/power-menu.ui:231 +msgid "Emergency" +msgstr "आपातकाल" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "कमांड चलाएं" + +#: src/ui/settings.ui:329 +msgid "No notifications" +msgstr "कोई अधिसूचना नहीं" + +#: src/ui/settings.ui:360 +msgid "Notifications" +msgstr "अधिसूचनाएं" + +#: src/ui/settings.ui:369 +msgid "Clear all" +msgstr "सभी साफ करें" + +#: src/ui/system-prompt.ui:57 +msgid "Confirm:" +msgstr "पुष्टि करें:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "पावर बंद करें (_P)…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "पुनः आरंभ करें (_R)…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "निलंबित करें (_S)…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "लॉग आउट (_L)…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "वाई-फाई" + +#: src/ui/wifi-status-page.ui:68 +msgid "Wi-Fi Settings" +msgstr "वाई-फाई सेटिंग्स" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "वीपीएन" + +# Adapted for Hindi Locale. +# Scrambled777 +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:222 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "प्लगिन नहीं मिला" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "प्लगिन '%s' लोड नहीं किया जा सका।" + +#: src/wifi-status-page.c:51 +msgid "No Wi-Fi Device Found" +msgstr "कोई वाई-फ़ाई उपकरण नहीं मिला" + +#: src/wifi-status-page.c:55 +msgid "Wi-Fi Disabled" +msgstr "वाई-फाई अक्षम" + +#: src/wifi-status-page.c:56 +msgid "Enable Wi-Fi" +msgstr "वाई-फाई सक्षम करें" + +#: src/wifi-status-page.c:59 +msgid "Wi-Fi Hotspot Active" +msgstr "वाई-फाई हॉटस्पॉट सक्रिय" + +#: src/wifi-status-page.c:60 +msgid "Turn Off" +msgstr "बंद करें" + +#: src/wifi-status-page.c:63 +msgid "No Wi-Fi Hotspots" +msgstr "कोई वाई-फाई हॉटस्पॉट नहीं" + +#: src/wifi-status-page.c:64 +msgid "Scan" +msgstr "स्कैन" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "सेलुलर" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:52 +msgid "Phosh on caffeine" +msgstr "कैफ़ीन पर फ़ॉश" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "On" +msgstr "सक्रिय" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "Off" +msgstr "अक्षम" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Default style" +msgstr "तयशुदा शैली" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Dark mode" +msgstr "गहरा मोड" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Light mode" +msgstr "हल्का मोड" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "व्यक्तिगत जानकारी" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "जन्म तिथि" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "मुख्य भाषा" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "घर का पता" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:130 +msgid "Medical Information" +msgstr "चिकित्सा संबंधी जानकारी" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:136 +msgid "Age" +msgstr "उम्र" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:148 +msgid "Blood Type" +msgstr "रक्त प्रकार" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:160 +msgid "Height" +msgstr "कद" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:172 +msgid "Weight" +msgstr "वजन" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:184 +msgid "Allergies" +msgstr "एलर्जी" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "दवाएं एवं शर्तें" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:216 +msgid "Other Information" +msgstr "अन्य जानकारी" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "आपातकालीन जानकारी प्राथमिकताएं" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "सम्पन्न" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "मालिक का नाम" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:200 +msgid "Medications and Conditions" +msgstr "दवाएं और शर्तें" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:243 +msgid "Add Contact" +msgstr "संपर्क जोड़ें" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:288 +msgid "Add New Contact" +msgstr "नए संपर्क को जोड़े" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:298 +msgid "Add" +msgstr "जोड़ें" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:319 +msgid "New Contact Name" +msgstr "नए संपर्क का नाम" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:333 +msgid "Relationship" +msgstr "रिश्ता" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:346 +msgid "Number" +msgstr "नंबर" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "कोई प्रक्षेपक विन्यस्त नहीं किया गया" + +#: plugins/launcher-box/launcher-box.ui:32 +msgid "Launchers" +msgstr "प्रक्षेपक" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:66 +msgid "Mobile Data On" +msgstr "मोबाइल डेटा चालू" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:66 +msgid "Mobile Data Off" +msgstr "मोबाइल डेटा बंद" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:64 +msgid "Night Light On" +msgstr "रात्रि प्रकाश चालू" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:64 +msgid "Night Light Off" +msgstr "रात्रि प्रकाश बंद" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "दिखाने के लिए कोई दस्तावेज नहीं है" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "टिकटें" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "फोल्डर चुनें" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "खोलें (_O)" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "टिकट बॉक्स प्राथमिकताएं" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "पथ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "फोल्डर सेटिंग्स" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "जहां फ़ॉश आपके टिकट खोजता है" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "टिकट फोल्डर" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "आज" + +# Since an upcoming event can not be in past, we need not specify this 'कल' is 'tomorrow' only +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "कल" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "%d दिन में" +msgstr[1] "%d दिनों में" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "कोई कार्यक्रम नहीं" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "पूरा दिन" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "समाप्त होता है" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "अनाम कार्यक्रम" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:62 +msgid "Hotspot On" +msgstr "हॉटस्पॉट चालू" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:64 +msgid "Hotspot Off" +msgstr "हॉटस्पॉट बंद" diff --git a/po/hr.po b/po/hr.po new file mode 100644 index 000000000..3a68277df --- /dev/null +++ b/po/hr.po @@ -0,0 +1,643 @@ +# Croatian translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-01-11 20:25+0000\n" +"PO-Revision-Date: 2023-01-13 12:26+0100\n" +"Last-Translator: gogo \n" +"Language-Team: Croatian \n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.2.2\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Telefonska ljuska" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Upravljanje prozorima i aplikacijama na mobilnim telefonima" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Kalendar" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Jednostavna aplikacija kalendara" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Kutija za karte" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Prikaži PDF-ove na zaključanom zaslonu. Ovaj priključak je " +"eksperimentalan." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Nadolazeći događaji" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Prikaži nadolazeće događaje" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Aplikacije" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Prikaži sve aplikacije" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Prikaži samo mobilne aplikacije" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Uključeno" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Pričvršećeno" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Nije pričvršećeno" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Odjava" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s će se automatski odjaviti za %d sekundu." +msgstr[1] "%s će se automatski odjaviti za %d sekunde." +msgstr[2] "%s će se automatski odjaviti za %d sekundi." + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Isključi" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sustav će se automatski isključiti za %d sekundu." +msgstr[1] "Sustav će se automatski isključiti za %d sekunde." +msgstr[2] "Sustav će se automatski isključiti za %d sekundi." + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Ponovno pokreni" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sustav će se automatski ponovno pokrenuti za %d sekundu." +msgstr[1] "Sustav će se automatski ponovno pokrenuti za %d sekunde." +msgstr[2] "Sustav će se automatski ponovno pokrenuti za %d sekundi." + +#: src/end-session-dialog.c:269 +msgid "Unknown application" +msgstr "Nepoznata aplikacija" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:91 +msgid "Quiet" +msgstr "Neprimjetno" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Silent" +msgstr "Utišano" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:101 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Uključeno" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Dopustite da '%s' pristupi vašim informacijama lokacije?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolokacija" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Da" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Ne" + +#: src/lockscreen.c:169 src/ui/lockscreen.ui:232 +msgid "Enter Passcode" +msgstr "Upiši lozinku" + +#: src/lockscreen.c:393 +msgid "Checking…" +msgstr "Provjeravanje…" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:182 +msgid "Unknown Title" +msgstr "Nepoznati naslov" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:165 +msgid "Unknown Artist" +msgstr "Nepoznati izvođač" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Ugrađeni zaslon" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Nepoznat" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Vrsta ovjere bežične mreže “%s” nije podržana" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Upišite lozinku za bežičnu mrežu “%s”" + +#: src/notifications/mount-notification.c:122 +msgid "Open" +msgstr "Otvori" + +#: src/notifications/notification.c:383 src/notifications/notification.c:639 +msgid "Notification" +msgstr "Obavijest" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "sek" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "min" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "min" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "sat" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "sata" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "dan" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "dana" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "mje" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mje" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "god" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "god" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "sada" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Preko %dgod" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Zamalo %dgod" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:228 +msgid "Authentication dialog was dismissed by the user" +msgstr "Dijalog ovjere je prekinut od strane korisnika" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Lozinka:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Nažalost, to ne radi. Pokušajte ponovno." + +#: src/rotateinfo.c:81 +msgid "Portrait" +msgstr "Uspravno" + +#: src/rotateinfo.c:84 +msgid "Landscape" +msgstr "Položeno" + +#: src/rotateinfo.c:104 src/rotateinfo.c:188 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Isključeno" + +#: src/rotateinfo.c:105 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Uključeno" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Pritisni ESC za zatvaranje" + +#: src/run-command-manager.c:95 +#, c-format +msgid "Running '%s' failed" +msgstr "Pokretanje naredbe '%s' neuspjelo" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Lozinke se ne podudaraju." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Lozinka ne može biti prazna" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Svjetlo" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Zapamti odabir" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:53 +msgid "Cancel" +msgstr "Odustani" + +#: src/ui/app-auth-prompt.ui:71 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "U redu" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Aplikacija" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Ukloni iz _omiljenih" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Dodaj u _omiljene" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Pogledaj _pojedinosti" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Pretraži aplikacije…" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Neke aplikacije su zauzete ili imaju nespremljeni rad" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Korisnik:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Domena:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "Po_veži se" + +#: src/ui/lockscreen.ui:39 src/ui/lockscreen.ui:340 +msgid "Back" +msgstr "Natrag" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Povuci za otključavanje" + +#: src/ui/lockscreen.ui:282 +msgid "Emergency" +msgstr "Hitno" + +#: src/ui/lockscreen.ui:298 +msgid "Unlock" +msgstr "Otključaj" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Potrebna je ovjera" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:84 +msgid "_Cancel" +msgstr "_Odustani" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "P_oveži se" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Ovjera" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Pokreni naredbu" + +#: src/ui/settings.ui:302 +msgid "No notifications" +msgstr "Nema obavijesti" + +#: src/ui/settings.ui:343 +msgid "Clear all" +msgstr "Ukloni sve" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Potvrdi:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Zaključavanje zaslona" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Odjava" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:319 +msgid "%A, %B %-e" +msgstr "%A, %-d. %B" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Priključak nije pronađen" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Priključak '%s' se ne može učitati." + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Bežična mreža" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:200 +msgid "Cellular" +msgstr "Mobitel" + +#: plugins/emergency-info/emergency-info.ui:39 +msgid "Personal Information" +msgstr "Osobne informacije" + +#: plugins/emergency-info/emergency-info.ui:47 +msgid "Date of Birth" +msgstr "Datum rođenja" + +#: plugins/emergency-info/emergency-info.ui:65 +msgid "Preferred Language" +msgstr "Željeni jezik" + +#: plugins/emergency-info/emergency-info.ui:83 +msgid "Home Address" +msgstr "Kućna adresa" + +#: plugins/emergency-info/emergency-info.ui:91 +msgid "Medical Information" +msgstr "Medicinske informacje" + +#: plugins/emergency-info/emergency-info.ui:99 +msgid "Age" +msgstr "Dob" + +#: plugins/emergency-info/emergency-info.ui:117 +msgid "Blood Type" +msgstr "Krvna grupa" + +#: plugins/emergency-info/emergency-info.ui:135 +msgid "Height" +msgstr "Visina" + +#: plugins/emergency-info/emergency-info.ui:153 +msgid "Weight" +msgstr "Težina" + +#: plugins/emergency-info/emergency-info.ui:171 +msgid "Allergies" +msgstr "Alergije" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Lijekovi i bolesti" + +#: plugins/emergency-info/emergency-info.ui:187 +msgid "Other Information" +msgstr "Ostale informacije" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Emergency Contacts" +msgstr "Hitni kontakt" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Nema dokumenata za prikaz" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Kartice" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:81 +msgid "Choose Folder" +msgstr "Odaberi mapu" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:85 +msgid "_Open" +msgstr "_Otvori" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Osobitosti kutija za karte" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Putanje" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Postavke mape" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Gdje Phosh traži vaše karte" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Mapa karta" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Danas" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Sutra" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "Za %d dan" +msgstr[1] "Za %d dana" +msgstr[2] "Za %d dana" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Nema događaja" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Cijeli dan" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Završava u" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Bezimeni događaj" + +#, c-format +#~ msgid "On %A" +#~ msgstr "U %A" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Prikaži samo adaptivne aplikacije" + +#~ msgid "Suspend" +#~ msgstr "Suspenzija" diff --git a/po/ht.po b/po/ht.po new file mode 100644 index 000000000..699e27d23 --- /dev/null +++ b/po/ht.po @@ -0,0 +1,1128 @@ +# Phosh Haitian Creole Translation +# Copyright (C) 2024 Purism +# This file is distributed under the same license as the Phosh package. +# Pierre Michel Augustin , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2024-11-11 09:22+0000\n" +"PO-Revision-Date: 2024-11-18 09:37-0500\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: ht_HT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 2.4.2\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Shell Telefòn" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Jesyon fenèt ak lansman aplikasyon pou mobil" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Sesyon sa a konekte ou nan Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Kafeyin Anviwònman Rapid" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Anpeche sesyon an ale san fè anyen konsa" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Kalandriye" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Yon senp widget kalandriye" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Mòd nwa / Koulè Scheme Anviwònman rapid" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Aktive mòd nwa" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Enfòmasyon ijans" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Montre enfòmasyon ak kontak ijans" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Bwat Lansè" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "Ajoute lans nan ekran fèmen an. Plugin sa a se eksperimantal." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Mobil Done Paramèt Rapid" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Aktive/dezaktive done mobil" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Paramèt Rapid Limyè Lannwit" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Aktive/dezaktive limyè lannwit" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro Paramèt Rapid" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Senp Pomodoro revèy" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Bwat Tikè" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Montre PDF yo sou ekran fèmen a. Plugin sa a se eksperimantal." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Evènman k ap vini yo" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Montre evènman kalandriye k ap vini yo" + +#: src/app-grid-button.c:153 +msgid "Add to Folder" +msgstr "Ajoute nan Dosye" + +#: src/app-grid-button.c:177 +msgid "Create new folder" +msgstr "Kreye nouvo dosye" + +#: src/app-grid-button.c:692 src/app-grid-button.c:749 +msgid "Application" +msgstr "Aplikasyon" + +#: src/app-grid.c:263 +msgid "Show All Apps" +msgstr "Montre tout aplikasyon yo" + +#: src/app-grid.c:266 +msgid "Show Only Mobile Friendly Apps" +msgstr "Montre sèlman aplikasyon pou mobil zanmitay" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:72 +#, c-format +msgid "Battery %.0f%%" +msgstr "" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Sou" + +#: src/bt-status-page.c:95 +msgid "No connectable Bluetooth Devices found" +msgstr "Yo pa jwenn okenn aparèy Bluetooth ki kapab konekte" + +#: src/bt-status-page.c:99 +msgid "Bluetooth disabled" +msgstr "Bluetooth dezaktive" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Apèl enkoni" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Docked" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Undocked" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "Ok" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Pa kapab fè apèl ijans" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Erè entèn" + +#: src/end-session-dialog.c:174 +msgid "Log Out" +msgstr "Dekonekte" + +#: src/end-session-dialog.c:177 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s pral dekonekte otomatikman nan %d segonn." +msgstr[1] "%s pral dekonekte otomatikman nan %d segonn." + +#: src/end-session-dialog.c:183 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Fèmen" + +#: src/end-session-dialog.c:184 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistèm nan ap koupe otomatikman nan %d segonn." +msgstr[1] "Sistèm nan ap koupe otomatikman nan %d segonn." + +#: src/end-session-dialog.c:190 +msgid "Restart" +msgstr "Redemare" + +#: src/end-session-dialog.c:191 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistèm nan ap redemare otomatikman nan %d segonn." +msgstr[1] "Sistèm nan ap rekòmanse otomatikman nan %d segonn." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Trankil" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silans" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Sou" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Pèmèt '%s' jwenn aksè nan enfòmasyon kote ou a?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Jeolokalizasyon" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Wi" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Non" + +#. give visual feedback on error +#: src/lockscreen.c:293 src/ui/lockscreen.ui:290 +msgid "Enter Passcode" +msgstr "Antre pas kod" + +#: src/lockscreen.c:949 +msgid "Checking…" +msgstr "Ap verifye…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:235 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Ekran sove nan '%s'" + +#: src/screenshot-manager.c:237 +msgid "Failed to save screenshot" +msgstr "Echwe pou sove ekran an" + +#: src/screenshot-manager.c:241 src/ui/power-menu.ui:192 +msgid "Screenshot" +msgstr "Foto ekran" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:416 +msgid "Screenshots" +msgstr "Foto ekran yo" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:436 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Ekran soti nan %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:446 src/ui/media-player.ui:213 +msgid "Unknown Title" +msgstr "Tit enkoni" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:454 src/ui/media-player.ui:201 +msgid "Unknown Artist" +msgstr "Atis enkoni" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Montre Built-in" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, fuzzy, c-format +#| msgctxt "" +#| "This is a monitor vendor name, followed by a size in inches, like 'Dell " +#| "15\"'" +#| msgid "%s %s" +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Enkoni" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Kalite otantifikasyon rezo Wi-Fi “%s” pa sipòte" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Antre modpas pou rezo Wi-Fi \"%s\"" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Ouvri" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:404 src/notifications/notification.c:677 +#: src/notifications/notify-manager.c:1004 +msgid "Notification" +msgstr "Notifikasyon" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:86 +msgid "now" +msgstr "kounye a" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:91 +#, c-format +msgid "<30s" +msgstr "" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:96 +#, c-format +msgid "<1m" +msgstr "" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:101 +#, c-format +msgid "~1m" +msgstr "" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:109 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "" +msgstr[1] "" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:117 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "" +msgstr[1] "" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:122 +#, c-format +msgid "~1d" +msgstr "" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:127 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "" +msgstr[1] "" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:132 +#, c-format +msgid "~1mo" +msgstr "" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:137 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "" +msgstr[1] "" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:147 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "" +msgstr[1] "" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:151 +#, fuzzy, c-format +#| msgid "Over %dy" +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Plis pase %dy" +msgstr[1] "Plis pase %dy" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:156 +#, fuzzy, c-format +#| msgid "Almost %dy" +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Prèske %dy" +msgstr[1] "Prèske %dy" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Dyalòg Otantifikasyon te ranvwaye pa itilizatè a" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Mo de pas:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Eskize, sa pa te mache. Souple eseye ankò." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Pòtrè" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Peyizaj" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Fèmen" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Sou" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Peze ESC pou fèmen" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Kouri '%s' echwe" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Kontwòl volim koki telefòn" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Mo de pas pa matche ak." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Mo de pas pa ka vid" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Flach" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Sonje desizyon" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:293 +msgid "Cancel" +msgstr "Anile" + +#: src/ui/app-grid-button.ui:23 +msgid "Remove from _Favorites" +msgstr "Retire nan sa ou _Renmen yo" + +#: src/ui/app-grid-button.ui:28 +msgid "Add to _Favorites" +msgstr "Ajoute nan sa ou _Renmen yo" + +#: src/ui/app-grid-button.ui:33 +msgid "View _Details" +msgstr "Gade _Detay yo" + +#: src/ui/app-grid-button.ui:41 +msgid "_Remove from Folder" +msgstr "_Retire nan Dosye" + +#: src/ui/app-grid.ui:24 +msgid "Search apps…" +msgstr "Chèche aplikasyon yo…" + +#: src/ui/audio-settings.ui:85 +msgid "Output Devices" +msgstr "Sòti Aparèy" + +#: src/ui/audio-settings.ui:109 +msgid "Input Devices" +msgstr "Antre Aparèy" + +#: src/ui/audio-settings.ui:137 +msgid "Sound Settings" +msgstr "Paramèt Son" + +#: src/ui/bt-status-page.ui:31 +msgid "Enable Bluetooth" +msgstr "Aktive Bluetooth" + +#: src/ui/bt-status-page.ui:55 +msgid "Bluetooth Settings" +msgstr "Paramèt Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Fèmen dyalòg apèl ijans lan" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Ijans _Kontak" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Ale nan paj kontak ijans yo" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Retounen nan paj dialpad ijans lan" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Pwopriyetè enkoni" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:234 +msgid "Emergency Contacts" +msgstr "Kontak Ijans" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Pa gen kontak ijans ki disponib." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Gen kèk aplikasyon ki okipe oswa ki gen travay ki pa sove" + +#: src/ui/gtk-mount-prompt.ui:88 +msgid "User:" +msgstr "Itilizatè:" + +#: src/ui/gtk-mount-prompt.ui:111 +msgid "Domain:" +msgstr "Domèn:" + +#: src/ui/gtk-mount-prompt.ui:144 +msgid "Co_nnect" +msgstr "Ko_nekte" + +#: src/ui/lockscreen.ui:47 src/ui/lockscreen.ui:256 +msgid "Back" +msgstr "Retounen" + +#: src/ui/lockscreen.ui:98 +msgid "Slide up to unlock" +msgstr "Monte pou debloke" + +#: src/ui/lockscreen.ui:341 +msgid "Unlock" +msgstr "Debloke" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Otantifikasyon obligatwa" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Anile" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "K_onekte" + +#: src/ui/polkit-auth-prompt.ui:117 +msgid "Authenticate" +msgstr "Otantifye" + +#: src/ui/power-menu.ui:109 +msgid "Suspend" +msgstr "Sispann" + +#: src/ui/power-menu.ui:153 +msgid "Lock" +msgstr "Bloke" + +#: src/ui/power-menu.ui:231 +msgid "Emergency" +msgstr "Ijans" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Egzekite Kòmand" + +#: src/ui/settings.ui:142 +msgid "No notifications" +msgstr "Pa gen notifikasyon" + +#: src/ui/settings.ui:173 +msgid "Notifications" +msgstr "Notifikasyon yo" + +#: src/ui/settings.ui:182 +msgid "Clear all" +msgstr "Efase tout" + +#: src/ui/system-prompt.ui:57 +msgid "Confirm:" +msgstr "Konfime:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "_Fèmen…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "_Redemare…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Sispann…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Dekonekte…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:72 +msgid "Wi-Fi Settings" +msgstr "Paramèt Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Pa jwenn Plugin" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Plugin '%s' pa te kapab chaje." + +#: src/wifi-status-page.c:51 +msgid "No Wi-Fi Device Found" +msgstr "Pa gen okenn aparèy Wi-Fi jwenn" + +#: src/wifi-status-page.c:55 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi Dezaktive" + +#: src/wifi-status-page.c:56 +msgid "Enable Wi-Fi" +msgstr "Wi-Fi Aktive" + +#: src/wifi-status-page.c:59 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi Hotspot aktive" + +#: src/wifi-status-page.c:60 +msgid "Turn Off" +msgstr "Fèmen" + +#: src/wifi-status-page.c:63 +msgid "No Wi-Fi Hotspots" +msgstr "Pa gen Wi-Fi Hotspots" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Selilè" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh sou kafeyin" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Ouvri" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +#, fuzzy +#| msgctxt "automatic-screen-rotation-disabled" +#| msgid "Off" +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Fèmen" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Stil Defo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Mòd nwa" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Mòd Klè" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Enfomasyon Pèsonèl" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Dat Nesans" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Lang Prefere" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Adrès Lakay" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:130 +msgid "Medical Information" +msgstr "Enfòmasyon Medikal" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:136 +msgid "Age" +msgstr "Laj" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:148 +msgid "Blood Type" +msgstr "Tip San" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:160 +msgid "Height" +msgstr "Wotè" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:172 +msgid "Weight" +msgstr "Pwa" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:184 +msgid "Allergies" +msgstr "Alèji" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Medikaman yo & Kondisyon yo" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:216 +msgid "Other Information" +msgstr "Lòt Enfòmasyon" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Preferans Enfòmasyon pou Ijans" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Fè" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Non Pwopriyetè" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:200 +msgid "Medications and Conditions" +msgstr "Medikaman yo ak Kondisyon yo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:243 +msgid "Add Contact" +msgstr "Ajoute Kontak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:288 +msgid "Add New Contact" +msgstr "Ajoute nouvo kontak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:298 +msgid "Add" +msgstr "Ajoute" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:319 +msgid "New Contact Name" +msgstr "Nouvo Non Kontak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:333 +msgid "Relationship" +msgstr "Relasyon" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:346 +msgid "Number" +msgstr "Nimewo" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Pa gen lansè ki te konfigire" + +#: plugins/launcher-box/launcher-box.ui:32 +msgid "Launchers" +msgstr "Lansè" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Done mobil sou" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Done mobil koupe" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Limyè lannwit limen" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Limyè lannwit koupe" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Pomodoro kòmanse" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Konsantre sou travay ou pandan %d minit" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Pran yon ti repo" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Ou gen %d minit jiska pwochen Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Revèy Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Fèmen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro Preferans Paramèt Rapid" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "Pomodoro Technique" +msgstr "Teknik Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:17 +msgid "_Active Duration" +msgstr "_Dire aktif" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:18 +msgid "Duration of the focus session" +msgstr "Dire sesyon konsantre an" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:32 +msgid "_Break Duration" +msgstr "_Dire repo" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:33 +msgid "Duration of the break between sessions" +msgstr "Dire repo ant sesyon yo" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Pa gen dokiman pou montre" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Tikè yo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Chwazi Dosye" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Ouvri" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferans Bwat Tikè yo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Chemen yo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Paramèt Dosye yo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Ki kote Phosh ap chèche tikè ou yo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Tikè Dosye" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Jodia" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Demen" + +#: plugins/upcoming-events/event-list.c:150 +#, fuzzy, c-format +#| msgid "In %d day" +#| msgid_plural "In %d days" +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "Nan %d jou" +msgstr[1] "Nan %d jou yo" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Pa gen evènman" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tout jou" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Fini yo" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Evènman san tit" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferans evènman k ap vini yo" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:20 +msgid "Days" +msgstr "Jou" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:16 +msgid "Date Range" +msgstr "Entèval Dat" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Number of days to show events for" +msgstr "Kantite jou pou montre evènman yo" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Hotspot Ouvri" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Hotspot Fèmen" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mo" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mos" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "y" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "y" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplikasyon" + +#~ msgid "_Power Off" +#~ msgstr "_Fèmen" + +#~ msgid "_Screenshot" +#~ msgstr "_Foto ekran" + +#, c-format +#~ msgctxt "" +#~ "This is a monitor vendor name followed by product/model name where size " +#~ "ininches could not be calculated, e.g. Dell U2414H" +#~ msgid "%s %sn" +#~ msgstr "%s %sn" + +#~ msgid "Lock Screen" +#~ msgstr "Bloke ekran" + +#~ msgid "Logout" +#~ msgstr "Dekonekte" diff --git a/po/hu.po b/po/hu.po new file mode 100644 index 000000000..950836189 --- /dev/null +++ b/po/hu.po @@ -0,0 +1,971 @@ +# Hungarian translation for Phosh. +# Copyright (C) 2019, 2022, 2023, 2024 Free Software Foundation, Inc. +# This file is distributed under the same license as the phosh package. +# +# Csaba Fazekas , 2019. +# Balázs Úr , 2022, 2023, 2024. +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2024-09-09 09:28+0000\n" +"PO-Revision-Date: 2024-09-09 13:00+0200\n" +"Last-Translator: Balázs Úr \n" +"Language-Team: Hungarian \n" +"Language: hu\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 23.08.4\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Telefonos parancsértelmező" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Ablakkezelés és alkalmazásindítás mobilra" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Ez a munkamenet bejelentkezteti a Phosh-ba" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Koffein gyors beállítása" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "A munkamenet tétlen állapotba kerülésének megakadályozása" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Naptár" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Egyszerű naptár felületi elem" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Sötét mód / színséma gyors beállítása" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Sötét mód be- és kikapcsolása" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Vészhelyzeti információk" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Vészhelyzeti információk és partnerek megjelenítése" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Indítódoboz" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Indítók hozzáadása a zárolási képernyőhöz. Ez a bővítmény kísérleti." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Mobil adatkapcsolat gyors beállítása" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Mobil adatkapcsolat be- és kikapcsolása" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Éjszakai fény gyors beállítása" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Éjszakai fény be- és kikapcsolása" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Jegypénztár" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"PDF-ek megjelenítése a zárolási képernyőn. Ez a bővítmény kísérleti." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Közelgő események" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Közelgő naptáresemények megjelenítése" + +#: src/app-grid-button.c:153 +msgid "Add to Folder" +msgstr "Hozzáadás mappához" + +#: src/app-grid-button.c:177 +msgid "Create new folder" +msgstr "Új mappa létrehozása" + +#: src/app-grid-button.c:692 src/app-grid-button.c:749 +msgid "Application" +msgstr "Alkalmazás" + +#: src/app-grid.c:263 +msgid "Show All Apps" +msgstr "Összes alkalmazás megjelenítése" + +#: src/app-grid.c:266 +msgid "Show Only Mobile Friendly Apps" +msgstr "Csak mobilbarát alkalmazások megjelenítése" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:72 +#, c-format +msgid "Battery %.0f%%" +msgstr "Akkumulátor %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Be" + +#: src/bt-status-page.c:86 +msgid "No connectable Bluetooth Devices found" +msgstr "Nem található csatlakoztatható Bluetooth-eszköz" + +#: src/bt-status-page.c:90 +msgid "Bluetooth disabled" +msgstr "Bluetooth letiltva" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Ismeretlen hívó" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Dokkolva" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Dokkolás megszüntetve" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Nem lehet segélyhívást indítani" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Belső hiba" + +#: src/end-session-dialog.c:174 +msgid "Log Out" +msgstr "Kijelentkezés" + +#: src/end-session-dialog.c:177 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "A(z) %s automatikusan kijelentkezik %d másodperc múlva." +msgstr[1] "A(z) %s automatikusan kijelentkezik %d másodperc múlva." + +#: src/end-session-dialog.c:183 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Kikapcsolás" + +#: src/end-session-dialog.c:184 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "A rendszer automatikusan kikapcsol %d másodperc múlva." +msgstr[1] "A rendszer automatikusan kikapcsol %d másodperc múlva." + +#: src/end-session-dialog.c:190 +msgid "Restart" +msgstr "Újraindítás" + +#: src/end-session-dialog.c:191 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "A rendszer automatikusan újraindul %d másodperc múlva." +msgstr[1] "A rendszer automatikusan újraindul %d másodperc múlva." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Csendes" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Néma" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Be" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Engedélyezi a(z) „%s” számára, hogy hozzáférjen a helyinformációjához?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Földrajzi helymeghatározás" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Igen" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Nem" + +#. give visual feedback on error +#: src/lockscreen.c:273 src/ui/lockscreen.ui:246 +msgid "Enter Passcode" +msgstr "Adja meg a jelkódot" + +#: src/lockscreen.c:918 +msgid "Checking…" +msgstr "Ellenőrzés…" + +#: src/screenshot-manager.c:221 src/ui/power-menu.ui:192 +msgid "Screenshot" +msgstr "Képernyőkép" + +#: src/screenshot-manager.c:222 +msgid "Screenshot copied to clipboard" +msgstr "Képernyőkép vágólapra másolva" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:446 src/ui/media-player.ui:213 +msgid "Unknown Title" +msgstr "Ismeretlen cím" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:454 src/ui/media-player.ui:201 +msgid "Unknown Artist" +msgstr "Ismeretlen előadó" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Beépített kijelző" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Ismeretlen" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "A(z) „%s” Wi-Fi-hálózat hitelesítési típusa nem támogatott" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Adja meg a(z) „%s” Wi-Fi-hálózat jelszavát" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Nyitott" + +#: src/notifications/notification.c:404 src/notifications/notification.c:677 +msgid "Notification" +msgstr "Értesítés" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:86 +msgid "now" +msgstr "most" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:91 +#, c-format +msgid "<30s" +msgstr "<30 mp" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:96 +#, c-format +msgid "<1m" +msgstr "<1 p" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 7 chars +#: src/notifications/timestamp-label.c:101 +#, c-format +msgid "~1m" +msgstr "~1 p" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:109 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d p" +msgstr[1] "%d p" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:117 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d ó" +msgstr[1] "~%d ó" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:122 +#, c-format +msgid "~1d" +msgstr "~1 n" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:127 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d n" +msgstr[1] "%d n" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:132 +#, c-format +msgid "~1mo" +msgstr "~1 h" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:137 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d h" +msgstr[1] "%d h" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:147 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d é" +msgstr[1] "~%d é" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:151 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Több mint %d év" +msgstr[1] "Több mint %d év" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:156 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Majdnem %d év" +msgstr[1] "Majdnem %d év" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "A felhasználó bezárta a hitelesítési párbeszédablakot" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Jelszó:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Sajnáljuk, ez nem működött. Próbálja meg újra." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Álló" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Fekvő" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Ki" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Be" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Nyomja meg az ESC billentyűt a bezáráshoz" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "A(z) „%s” futtatása sikertelen" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Telefonkagyló hangerőszabályzó" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "A jelszavak nem egyeznek." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "A jelszó nem lehet üres" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Fáklya" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Emlékezzen a döntésre" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:293 +msgid "Cancel" +msgstr "Mégse" + +#: src/ui/app-grid-button.ui:23 +msgid "Remove from _Favorites" +msgstr "Eltávolítás a _kedvencek közül" + +#: src/ui/app-grid-button.ui:28 +msgid "Add to _Favorites" +msgstr "Hozzáadás a _kedvencekhez" + +#: src/ui/app-grid-button.ui:33 +msgid "View _Details" +msgstr "_Részletek megtekintése" + +#: src/ui/app-grid-button.ui:41 +msgid "_Remove from Folder" +msgstr "_Eltávolítás mappából" + +#: src/ui/app-grid.ui:24 +msgid "Search apps…" +msgstr "Alkalmazások keresése…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Kimeneti eszközök" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Bemeneti eszközök" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Hangbeállítások" + +#: src/ui/bt-status-page.ui:32 +msgid "Enable Bluetooth" +msgstr "Bluetooth engedélyezése" + +#: src/ui/bt-status-page.ui:53 +msgid "Bluetooth Settings" +msgstr "Bluetooth beállítások" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "A segélyhívás párbeszédablakának bezárása" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Vészhelyzeti _partnerek" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Ugrás a vészhelyzeti partnerek oldalra" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Ugrás vissza a vészhelyzeti tárcsázó oldalra" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Tulajdonos ismeretlen" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:234 +msgid "Emergency Contacts" +msgstr "Vészhelyzeti partnerek" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Nincsenek elérhető vészhelyzeti partnerek." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Néhány alkalmazás foglalt vagy mentetlen munkát tartalmaz" + +#: src/ui/gtk-mount-prompt.ui:88 +msgid "User:" +msgstr "Felhasználó:" + +#: src/ui/gtk-mount-prompt.ui:111 +msgid "Domain:" +msgstr "Tartomány:" + +#: src/ui/gtk-mount-prompt.ui:144 +msgid "Co_nnect" +msgstr "_Kapcsolódás" + +#: src/ui/lockscreen.ui:37 src/ui/lockscreen.ui:335 +msgid "Back" +msgstr "Vissza" + +#: src/ui/lockscreen.ui:98 +msgid "Slide up to unlock" +msgstr "Csúsztassa fel a feloldáshoz" + +#: src/ui/lockscreen.ui:298 +msgid "Unlock" +msgstr "Feloldás" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Hitelesítés szükséges" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Mégse" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "Kapcs_olódás" + +#: src/ui/polkit-auth-prompt.ui:117 +msgid "Authenticate" +msgstr "Hitelesítés" + +#: src/ui/power-menu.ui:109 +msgid "Suspend" +msgstr "Felfüggesztés" + +#: src/ui/power-menu.ui:153 +msgid "Lock" +msgstr "Zárolás" + +#: src/ui/power-menu.ui:231 +msgid "Emergency" +msgstr "Vészhelyzet" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Parancs futtatása" + +#: src/ui/settings.ui:329 +msgid "No notifications" +msgstr "Nincsenek értesítések" + +#: src/ui/settings.ui:360 +msgid "Notifications" +msgstr "Értesítések" + +#: src/ui/settings.ui:369 +msgid "Clear all" +msgstr "Összes törlése" + +#: src/ui/system-prompt.ui:57 +msgid "Confirm:" +msgstr "Megerősítés:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "_Kikapcsolás…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "Új_raindítás…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Felfüggesztés…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Kijelentkezés…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:68 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi beállítások" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%B %-d. %A" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "A bővítmény nem található" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "A(z) „%s” bővítményt nem sikerült betölteni." + +#: src/wifi-status-page.c:51 +msgid "No Wi-Fi Device Found" +msgstr "Nem található Wi-Fi-eszköz" + +#: src/wifi-status-page.c:55 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi letiltva" + +#: src/wifi-status-page.c:56 +msgid "Enable Wi-Fi" +msgstr "Wi-Fi engedélyezése" + +#: src/wifi-status-page.c:59 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi hozzáférési pont aktív" + +#: src/wifi-status-page.c:60 +msgid "Turn Off" +msgstr "Kikapcsolás" + +#: src/wifi-status-page.c:63 +msgid "No Wi-Fi Hotspots" +msgstr "Nincsenek Wi-Fi hozzáférési pontok" + +#: src/wifi-status-page.c:64 +msgid "Scan" +msgstr "Keresés" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobilhálózat" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:52 +msgid "Phosh on caffeine" +msgstr "Phosh a koffeinen" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "On" +msgstr "Be" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "Off" +msgstr "Ki" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Default style" +msgstr "Alapértelmezett stílus" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Dark mode" +msgstr "Sötét mód" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Light mode" +msgstr "Világos mód" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Személyes információk" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Születés dátuma" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Előnyben részesített nyelv" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Otthoni cím" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:130 +msgid "Medical Information" +msgstr "Egészségügyi információk" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:136 +msgid "Age" +msgstr "Életkor" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:148 +msgid "Blood Type" +msgstr "Vércsoport" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:160 +msgid "Height" +msgstr "Magasság" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:172 +msgid "Weight" +msgstr "Súly" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:184 +msgid "Allergies" +msgstr "Allergiák" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Gyógyszerek és körülmények" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:216 +msgid "Other Information" +msgstr "Egyéb információk" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Vészhelyzeti információk beállításai" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Kész" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Tulajdonos neve" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:200 +msgid "Medications and Conditions" +msgstr "Gyógyszerek és körülmények" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:243 +msgid "Add Contact" +msgstr "Partner hozzáadása" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:288 +msgid "Add New Contact" +msgstr "Új partner hozzáadása" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:298 +msgid "Add" +msgstr "Hozzáadás" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:319 +msgid "New Contact Name" +msgstr "Új partner neve" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:333 +msgid "Relationship" +msgstr "Kapcsolat" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:346 +msgid "Number" +msgstr "Szám" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Nincsenek indítók beállítva" + +#: plugins/launcher-box/launcher-box.ui:32 +msgid "Launchers" +msgstr "Indítók" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:66 +msgid "Mobile Data On" +msgstr "Mobil adatkapcsolat be" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:66 +msgid "Mobile Data Off" +msgstr "Mobil adatkapcsolat ki" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:64 +msgid "Night Light On" +msgstr "Éjszakai fény be" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:64 +msgid "Night Light Off" +msgstr "Éjszakai fény ki" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Nincsenek megjelenítendő dokumentumok" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Jegyek" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Mappa kiválasztása" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Megnyitás" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Jegypénztár beállításai" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Útvonalak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Mappa beállításai" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Hol keresse a Phosh a jegyeit" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Jegymappa" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Ma" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Holnap" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +#| msgid "In %d day" +#| msgid_plural "In %d days" +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "%u napon belül" +msgstr[1] "%u napon belül" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Nincsenek események" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%H:%M" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Egész nap" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Befejeződik" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Névtelen esemény" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +#| msgid "Upcoming Events" +msgid "Upcoming Events Preferences" +msgstr "Közelgő események beállításai" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:20 +msgid "Days" +msgstr "Napok" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:16 +msgid "Date Range" +msgstr "Dátumtartomány" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Number of days to show events for" +msgstr "A megjelenítendő események napjainak száma" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:78 +msgid "Hotspot On" +msgstr "Hozzáférési pont be" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot Off" +msgstr "Hozzáférési pont ki" + diff --git a/po/id.po b/po/id.po new file mode 100644 index 000000000..adca7f927 --- /dev/null +++ b/po/id.po @@ -0,0 +1,1083 @@ +# Indonesian translation of phosh +# Copyright (C) 2024 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# +# Andika Triwidada , 2024, 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-03-14 07:56+0000\n" +"PO-Revision-Date: 2025-03-19 22:28+0700\n" +"Last-Translator: Andika Triwidada \n" +"Language-Team: \n" +"Language: id\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 3.5\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Shell Telepon" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Manajemen jendela dan peluncuran aplikasi untuk telepon genggam" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Sesi ini memasukkanmu ke dalam Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Pengaturan Cepat Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Mencegah sesi menganggur" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Kalender" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Widget kalender sederhana" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Mode Gelap / Pengaturan Cepat Skema Warna" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Jungkitkan mode gelap" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Info Darurat" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Menampilkan informasi dan kontak darurat" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Kotak Peluncur" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Tambahkan peluncur ke layar kunci. Plugin ini bersifat eksperimental." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Pengaturan Cepat Data Seluler" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Jungkitkan data seluler nyala/mati" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Pengaturan Cepat Cahaya Malam" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Jungkitkan cahaya malam nyala/mati" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Pengaturan Cepat Podomoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Pengatur Waktu Pomodoro Sederhana" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Kotak Tiket" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Tampilkan PDF pada layar kunci. pengaya ini dlm percobaan " + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Agenda Mendatang" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Tampilkan agenda kalender mendatang" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Tambahkan ke Folder" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Buat folder baru" + +#: src/app-grid-button.c:698 src/app-grid-button.c:754 +msgid "Application" +msgstr "Aplikasi" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Tampilkan Semua Aplikasi" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Tampilkan Hanya Apl Ramah Telepon" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Baterai %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Nyala" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Tidak ditemukan perangkat Bluetooth yang dapat dihubungkan" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth dinonaktifkan" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Penelepon tidak dikenal" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Jaringan Wi-Fi '%s' menggunakan portal captive" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Jaringan Wi-Fi menggunakan portal captive" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Masuk ke jaringan Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Ditambatkan" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Lepas Tambat" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:22 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Oke" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Tidak dapat melakukan panggilan darurat" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Galat internal" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Keluar" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s akan dikeluarkan secara otomatis dalam %d detik." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:69 +msgid "Power Off" +msgstr "Matikan Daya" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistem akan dimatikan secara otomatis dalam %d detik." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Mulai Ulang" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistem akan dinyalakan ulang secara otomatis dalam %d detik." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Hening" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Diam" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Nyala" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Izinkan '%s' mengakses informasi lokasimu?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolokasi" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Ya" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Tidak" + +#. give visual feedback on error +#: src/lockscreen.c:293 src/ui/lockscreen.ui:277 +msgid "Enter Passcode" +msgstr "Masukkan Kode Sandi" + +#: src/lockscreen.c:949 +msgid "Checking…" +msgstr "Memeriksa…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Cuplikan layar disimpan ke '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Gagal menyimpan cuplikan layar" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:187 +msgid "Screenshot" +msgstr "Cuplikan layar" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:527 +msgid "Screenshots" +msgstr "Cuplikan layar" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:547 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Cuplikan layar dari %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:493 src/ui/media-player.ui:211 +msgid "Unknown Title" +msgstr "Judul Tidak Dikenal" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:501 src/ui/media-player.ui:199 +msgid "Unknown Artist" +msgstr "Artis Tak Dikenal" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Tampilan bawaan" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Tak dikenal" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Jenis otentikasi jaringan Wi-Fi \"%s\" tidak didukung" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Masukkan kata sandi untuk jaringan Wi-Fi \"%s\"" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Buka" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1013 +msgid "Notification" +msgstr "Notifikasi" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "kini" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30d" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dj" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1hr" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dhr" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1bl" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dbl" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dth" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Lebih %dth" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Hampir %dth" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Dialog autentikasi ditutup oleh pengguna" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:77 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Kata Sandi:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Maaf, itu tidak bekerja. Harap coba lagi." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Potret" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Lanskap" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Mati" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Nyala" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Tekan ESC untuk tutup" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "'%s' gagal berjalan" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Kontrol Volume Shell Telepon" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Kata sandi tidak cocok." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Kata sandi tidak boleh kosong" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Senter" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Ingat pilihan ini" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Batal" + +#: src/ui/app-grid-button.ui:32 +msgid "Remove from _Favorites" +msgstr "Hapus dari _Favorit" + +#: src/ui/app-grid-button.ui:37 +msgid "Add to _Favorites" +msgstr "Tambah ke _Favorit" + +#: src/ui/app-grid-button.ui:42 +msgid "View _Details" +msgstr "Tampilkan _Rincian" + +#: src/ui/app-grid-button.ui:47 +msgid "Uninstall" +msgstr "Bongkar instalasi" + +#: src/ui/app-grid-button.ui:54 +msgid "_Remove from Folder" +msgstr "Hapus dari Folde_r" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Cari apl…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Perangkat Keluaran" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Perangkat Masukan" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Pengaturan Suara" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Aktifkan Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Pengaturan Bluetooth" + +#: src/ui/emergency-menu.ui:24 +msgid "Close the emergency call dialog" +msgstr "Tutup dialog panggilan darurat" + +#: src/ui/emergency-menu.ui:50 +msgid "Emergency _Contacts" +msgstr "_Kontak Darurat" + +#: src/ui/emergency-menu.ui:57 +msgid "Go to the emergency contacts page" +msgstr "Masuk ke halaman kontak darurat" + +#: src/ui/emergency-menu.ui:80 +msgid "Go back to the emergency dialpad page" +msgstr "Kembali ke halaman dialpad darurat" + +#: src/ui/emergency-menu.ui:103 +msgid "Owner unknown" +msgstr "Pemilik tidak diketahui" + +#: src/ui/emergency-menu.ui:121 plugins/emergency-info/emergency-info.ui:208 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Kontak Darurat" + +#: src/ui/emergency-menu.ui:139 +msgid "No emergency contacts available." +msgstr "Tidak ada kontak darurat yang tersedia." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Beberapa aplikasi sibuk atau belum disimpan hasilnya" + +#: src/ui/gtk-mount-prompt.ui:77 +msgid "User:" +msgstr "Pengguna:" + +#: src/ui/gtk-mount-prompt.ui:99 +msgid "Domain:" +msgstr "Domain:" + +#: src/ui/gtk-mount-prompt.ui:131 +msgid "Co_nnect" +msgstr "Sa_mbung" + +#: src/ui/lockscreen.ui:42 src/ui/lockscreen.ui:243 +msgid "Back" +msgstr "Kembali" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Usap naik untuk membuka kunci" + +#: src/ui/lockscreen.ui:327 +msgid "Unlock" +msgstr "Buka Kunci" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Perlu autentikasi" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Batal" + +#: src/ui/network-auth-prompt.ui:54 +msgid "C_onnect" +msgstr "Sa_mbung" + +#: src/ui/polkit-auth-prompt.ui:100 +msgid "Authenticate" +msgstr "Autentikasi" + +#: src/ui/power-menu.ui:106 +msgid "Suspend" +msgstr "Suspensi" + +#: src/ui/power-menu.ui:149 +msgid "Lock" +msgstr "Kunci" + +#: src/ui/power-menu.ui:225 +msgid "Emergency" +msgstr "Darurat" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Jalankan Perintah" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Tidak ada notifikasi" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Notifikasi" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Hilangkan semua" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Konfirmasi:" + +#: src/ui/top-panel.ui:31 +msgid "_Power Off…" +msgstr "_Matikan Daya…" + +#: src/ui/top-panel.ui:58 +msgid "_Restart…" +msgstr "_Mulai Ulang…" + +#: src/ui/top-panel.ui:85 +msgid "_Suspend…" +msgstr "_Suspensi…" + +#: src/ui/top-panel.ui:112 +msgid "_Log Out…" +msgstr "_Log Keluar…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:70 +msgid "Wi-Fi Settings" +msgstr "Pengaturan Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A, %d %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Pengaya tidak ditemukan" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Pengaya '%s' tidak bisa dimuat." + +#: src/wifi-status-page.c:52 +msgid "No Wi-Fi Device Found" +msgstr "Perangkat Wi-Fi Tidak Ditemukan" + +#: src/wifi-status-page.c:56 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi Dinonaktifkan" + +#: src/wifi-status-page.c:57 +msgid "Enable Wi-Fi" +msgstr "Aktifkan Wi-Fi" + +#: src/wifi-status-page.c:60 +msgid "Wi-Fi Hotspot Active" +msgstr "Hotspot Wi-Fi Aktif" + +#: src/wifi-status-page.c:61 +msgid "Turn Off" +msgstr "Matikan" + +#: src/wifi-status-page.c:64 +msgid "No Wi-Fi Hotspots" +msgstr "Tidak ada Hotspot Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Selular" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh pada caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Nyala" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Mati" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Gaya baku" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Mode gelap" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Mode terang" + +#: plugins/emergency-info/emergency-info.ui:40 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informasi Pribadi" + +#: plugins/emergency-info/emergency-info.ui:48 +msgid "Date of Birth" +msgstr "Tanggal Lahir" + +#: plugins/emergency-info/emergency-info.ui:68 +msgid "Preferred Language" +msgstr "Bahasa yang Disarankan" + +#: plugins/emergency-info/emergency-info.ui:88 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Alamat Rumah" + +#: plugins/emergency-info/emergency-info.ui:96 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informasi Kesehatan" + +#: plugins/emergency-info/emergency-info.ui:104 +msgid "Age" +msgstr "Umur" + +#: plugins/emergency-info/emergency-info.ui:124 +msgid "Blood Type" +msgstr "Jenis Darah" + +#: plugins/emergency-info/emergency-info.ui:144 +msgid "Height" +msgstr "Tinggi" + +#: plugins/emergency-info/emergency-info.ui:164 +msgid "Weight" +msgstr "Berat" + +#: plugins/emergency-info/emergency-info.ui:184 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergi" + +#: plugins/emergency-info/emergency-info.ui:192 +msgid "Medications & Conditions" +msgstr "Medikasi & Kondisi" + +#: plugins/emergency-info/emergency-info.ui:200 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Informasi Lainnya" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferensi Informasi Darurat" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Selesai" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "N_ama Pemilik" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Tan_ggal Lahir" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Bahasa yang _Disukai" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Usi_a" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Golongan darah" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Tinggi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Berat" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medikasi dan Kondisi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Tambah Kontak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Tambah Kontak Baru" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "T_ambah" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Nama _Kontak" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Hubungan" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Nomor Kontak" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Tidak ada peluncur yang dikonfigurasi" + +#: plugins/launcher-box/launcher-box.ui:30 +msgid "Launchers" +msgstr "Peluncur" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Data Seluler Aktif" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Data Seluler Mati" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Cahaya Malam Hidup" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Cahaya Malam Mati" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Mulai Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Fokus pada tugas Anda selama %d menit" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Beristirahatlah" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Anda memiliki %d menit hingga Pomodoro berikutnya" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Pencatat Waktu Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Mati" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Preferensi Pengaturan Cepat Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Teknik Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Durasi _Aktif" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Durasi sesi fokus" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Durasi Istirahat" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Durasi istirahat antar sesi" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Tidak ada dokumen untuk ditampilkan" + +#: plugins/ticket-box/ticket-box.ui:78 +msgid "Tickets" +msgstr "Tiket" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Buka" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Pilih Folder" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferensi Kotak Tiket" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Jalur" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Pengaturan Folder" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Di mana Phosh mencari tiketmu" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Folder Tiket" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hari ini" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Besok" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "Dalam %u hari" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Tidak ada acara" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Semua hari" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Berakhir" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Acara tanpa judul" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferensi Acara Mendatang" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Hari" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Rentang Tanggal" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Berapa hari untuk menampilkan acara" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Skala monitor" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Hotspot Nyala" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Hotspot Mati" diff --git a/po/it.po b/po/it.po new file mode 100644 index 000000000..29034c201 --- /dev/null +++ b/po/it.po @@ -0,0 +1,835 @@ +# Italian translation for phosh +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Vittorio Monti , 2024. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2024-03-13 16:31+0000\n" +"PO-Revision-Date: 2024-03-21 18:54+0100\n" +"Last-Translator: Vittorio Monti \n" +"Language-Team: Italian \n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.4.2\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Interfaccia per telefono" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Gestore di finestre e lanciatore di applicazioni per cellulare" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Questa sessione permette l'accesso a Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Opzioni per Caffeina" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Evita che la sessione diventi inattiva" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendario" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Un calendario in miniatura" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Info emergenza" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Mostra le informazioni di emergenza e i contatti" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Box lanciatore" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Aggiunge lanciatori sulla schermata di blocco. Questo plugin è " +"sperimentale." + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Biglietteria" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Mostra i PDF sulla schermata di blocco. Questo plugin è sperimentale." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Prossimi eventi" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Mostra i prossimi eventi del calendario" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Applicazione" + +#: src/app-grid.c:138 +msgid "Show All Apps" +msgstr "Mostra tutte le app" + +#: src/app-grid.c:141 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostra solo le app per cellulari" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Attivo" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/call-notification.c:61 +msgid "Unknown caller" +msgstr "Chiamante sconosciuto" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Agganciato" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Sganciato" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "Ok" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Impossibile effettuare chiamata di emergenza" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Errore interno" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Termina sessione" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s verrà disconnesso automaticamente fra %d secondo." +msgstr[1] "%s verrà disconnesso automaticamente fra %d secondi." + +#: src/end-session-dialog.c:182 +msgid "Power Off" +msgstr "Spegni" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Il sistema si spegnerà automaticamente fra %d secondo." +msgstr[1] "Il sistema si spegnerà automaticamente fra %d secondi." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Riavvia" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Il sistema verrà riavviato automaticamente fra %d secondo." +msgstr[1] "Il sistema verrà riavviato automaticamente fra %d secondi." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Muto" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silenzioso" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Attivo" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Consenti a '%s' di conoscere la tua posizione?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Posizione" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Sì" + +#: src/location-manager.c:274 +msgid "No" +msgstr "No" + +#: src/lockscreen.c:175 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "Inserire la password" + +#: src/lockscreen.c:401 +msgid "Checking…" +msgstr "Sto controllando…" + +#: src/screenshot-manager.c:213 +msgid "Screenshot" +msgstr "Schermata" + +#: src/screenshot-manager.c:214 +msgid "Screenshot copied to clipboard" +msgstr "Schermata copiata negli appunti" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:321 src/ui/media-player.ui:161 +msgid "Unknown Title" +msgstr "Titolo sconosciuto" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:329 src/ui/media-player.ui:148 +msgid "Unknown Artist" +msgstr "Artista sconosciuto" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Schermo integrato" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Sconosciuto" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Tipo di autenticazione della rete Wi-Fi “%s” non accettato" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Inserire la password per la rete Wi-Fi “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Apri" + +#: src/notifications/notification.c:405 src/notifications/notification.c:678 +msgid "Notification" +msgstr "Notifica" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "h" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "h" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "me" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mesi" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "a" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "a" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "adesso" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Oltre %da" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Quasi %da" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "La finestra di autenticazione è stata chiusa dall'utente" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Password:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Non ha funzionato. Riprovare." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Verticale" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Orizzontale" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Inattivo" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Attivo" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Premere ESC per chiudere" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Esecuzione di '%s' fallita" + +#: src/settings/audio-settings.c:373 +msgid "Phone Shell Volume Control" +msgstr "Controllo del volume" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Le password non corrispondono." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "La password non può essere vuota" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Torcia" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Ricorda la decisione" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:293 +msgid "Cancel" +msgstr "Annulla" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "App" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Rimuovi dai _Preferiti" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Aggiungi ai _Preferiti" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Visualizza _dettagli" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Ricerca app…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Dispositivi di uscita" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Dispositivi di ingresso" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Opzioni suono" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Chiudere la finestra della chiamata di emergenza" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Contatti di emergenza" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Vai alla pagina dei contatti di emergenza" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Torna alla pagina della chiamata di emergenza" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Proprietario sconosciuto" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:234 +msgid "Emergency Contacts" +msgstr "Contatti di emergenza" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Non ci sono contatti di emergenza." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Alcune applicazioni sono occupate o hanno del lavoro non salvato" + +#: src/ui/gtk-mount-prompt.ui:88 +msgid "User:" +msgstr "Utente:" + +#: src/ui/gtk-mount-prompt.ui:111 +msgid "Domain:" +msgstr "Dominio:" + +#: src/ui/gtk-mount-prompt.ui:144 +msgid "Co_nnect" +msgstr "Co_nnetti" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "Indietro" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "Scorri verso l'alto per sbloccare" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "Sblocca" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Autenticazione richiesta" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Annulla" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "C_onnetti" + +#: src/ui/polkit-auth-prompt.ui:117 +msgid "Authenticate" +msgstr "Autenticazione" + +#: src/ui/power-menu.ui:69 +msgid "_Power Off" +msgstr "S_pegni" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "B_locca" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "_Schermata" + +#: src/ui/power-menu.ui:192 +msgid "_Emergency" +msgstr "_Emergenza" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Esegui comando" + +#: src/ui/settings.ui:327 +msgid "No notifications" +msgstr "Nessuna notifica" + +#: src/ui/settings.ui:367 +msgid "Clear all" +msgstr "Cancella tutto" + +#: src/ui/system-prompt.ui:57 +msgid "Confirm:" +msgstr "Conferma:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "S_pegni…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "_Riavvia…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Sospendi…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Termina sessione…" + +#: src/ui/wifi-status-page.ui:32 +msgid "No Wi-Fi Device Found" +msgstr "Nessun dispositivo Wi-Fi trovato" + +#: src/ui/wifi-status-page.ui:60 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi disattivato" + +#: src/ui/wifi-status-page.ui:97 +msgid "Wi-Fi Hotspot Active" +msgstr "Hotspot Wi-Fi attivo" + +#: src/ui/wifi-status-page.ui:133 +msgid "No Wi-Fi Hotspots Found" +msgstr "Nessun Hotspot Wi-Fi trovato" + +#: src/ui/wifi-status-page.ui:144 +msgid "Scan" +msgstr "Cerca" + +#: src/ui/wifi-status-page.ui:176 +msgid "Wi-Fi Settings" +msgstr "Opzioni Wi-Fi" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:342 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Plugin non trovato" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Impossibile caricare il plugin '%s'." + +#: src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Cellulare" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:52 +msgid "Phosh on caffeine" +msgstr "Phosh con Caffeina" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "On" +msgstr "Attivo" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:131 +msgid "Off" +msgstr "Inattivo" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Informazioni personali" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Data di nascita" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Lingua preferita" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Indirizzo di residenza" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:130 +msgid "Medical Information" +msgstr "Informazioni mediche" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:136 +msgid "Age" +msgstr "Età" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:148 +msgid "Blood Type" +msgstr "Gruppo sanguigno" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:160 +msgid "Height" +msgstr "Altezza" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:172 +msgid "Weight" +msgstr "Peso" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:184 +msgid "Allergies" +msgstr "Allergie" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Medicine e Malattie" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:216 +msgid "Other Information" +msgstr "Altre informazioni" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Preferenze info emergenza" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Fatto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Nome del proprietario" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:200 +msgid "Medications and Conditions" +msgstr "Medicine e Malattie" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:243 +msgid "Add Contact" +msgstr "Aggiungi contatto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:288 +msgid "Add New Contact" +msgstr "Aggiungi nuovo contatto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:298 +msgid "Add" +msgstr "Aggiungi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:319 +msgid "New Contact Name" +msgstr "Nome del nuovo contatto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:333 +msgid "Relationship" +msgstr "Relazione" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:346 +msgid "Number" +msgstr "Numero" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Nessun lanciatore configurato" + +#: plugins/launcher-box/launcher-box.ui:32 +msgid "Launchers" +msgstr "Lanciatori" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Nessun documento da mostrare" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Biglietti" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Scegliere la cartella" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Apri" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferenze Biglietteria" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Percorsi" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Opzioni cartella" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Dove saranno cercati i biglietti" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Cartella Biglietti" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Oggi" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Domani" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "Fra %d giorno" +msgstr[1] "Fra %d giorni" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Nessun evento" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tutto il giorno" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Termina" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Evento senza titolo" diff --git a/po/ja.po b/po/ja.po new file mode 100644 index 000000000..8f7f01144 --- /dev/null +++ b/po/ja.po @@ -0,0 +1,403 @@ +# Japanese translation for phosh +# Copyright (C) 2018 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Scott Anecito , 2018. +# Guido Günther , 2018. #zanata +# Punleuk Oum , 2018. #zanata +# Scott , 2018. #zanata +# Scott Anecito , 2019. #zanata +# 寮 , 2021. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2021-01-16 10:30+0900\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2021-01-16 10:30+0900\n" +"Language-Team: \n" +"X-Generator: \n" +"Last-Translator: 寮 \n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Language: ja\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "モバイルでアプリケーションを整理したり、開いたりするためのグラフィカルシェル" + +#: src/app-grid-button.c:536 +msgid "Application" +msgstr "アプリケーション" + +#. Maintainers: while 普通 remains correct in context of normal, manner and silent manner modes, +#. it doesn't in the context of screen rotation. So I changed it to have it make kind of make sense for both. +#: src/bt-info.c:92 src/feedbackinfo.c:51 +msgid "On" +msgstr "有効" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "ドック中" + +#: src/docked-info.c:195 +msgid "Undocked" +msgstr "ドック外" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "ログアウト" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%2$d秒後、%1$sさんは自動でログアウトされます。" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "%d秒後、この端末は自動でシャットダウンされます。" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "%d秒後、この端末は自動で再起動されます。" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:44 +msgid "Quiet" +msgstr "マナーモード" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:49 +msgid "Silent" +msgstr "サイレントマナー" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "'%s'に位置情報へのアクセスを許可しますか?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "位置情報" + +#: src/location-manager.c:252 +#: src/location-manager.c:272 +msgid "Yes" +msgstr "はい" + +#: src/location-manager.c:252 +#: src/location-manager.c:272 +msgid "No" +msgstr "いいえ" + +#: src/lockscreen.c:86 src/ui/lockscreen.ui:234 +msgid "Enter Passcode" +msgstr "パスコードを入力" + +#: src/lockscreen.c:265 +msgid "Checking…" +msgstr "確認中…" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:343 +msgid "%A, %B %-e" +msgstr "%x(%a)" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:277 src/ui/media-player.ui:107 +msgid "Unknown Title" +msgstr "不明な曲" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:286 src/ui/media-player.ui:127 +msgid "Unknown Artist" +msgstr "不明なアーティスト" + +#: src/monitor-manager.c:71 +msgid "Built-in display" +msgstr "ビルトインディスプレイ" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:75 +msgid "Unknown" +msgstr "不明なディスプレー" + +#: src/end-session-dialog.c:270 +#| msgid "Application" +msgid "Unknown application" +msgstr "不明なアプリ" + +#: src/network-auth-prompt.c:187 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "「%s」のWiFi種類は非対応" + +#: src/network-auth-prompt.c:192 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "「%s」のWiFiパスワードを入力して下さい" + +#: src/notifications/mount-notification.c:137 +msgid "Open" +msgstr "開く" + +#: src/notifications/notification.c:381 src/notifications/notification.c:637 +msgid "Notification" +msgstr "通知" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "秒" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "分" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "分" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "時間" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "時間" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "日" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "日" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "月" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "月" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "年" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "年" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "只今" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "%dy以降" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "%dy頃" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:232 +msgid "Authentication dialog was dismissed by the user" +msgstr "ユーザーにより認証ダイアログを閉じた" + +#: src/polkit-auth-prompt.c:278 src/ui/network-auth-prompt.ui:130 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "パスワード:" + +#: src/polkit-auth-prompt.c:324 +msgid "Sorry, that didn’t work. Please try again." +msgstr "残念です。もう一度お試しください。" + +#: src/polkit-auth-prompt.c:469 +msgid "Authenticate" +msgstr "認証" + +#: src/rotateinfo.c:65 +msgid "Portrait" +msgstr "縦方向" + +#: src/rotateinfo.c:68 +msgid "Landscape" +msgstr "横方向" + +#. Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) +#. Translators: Automatic screen orientation is off (locked/disabled) +#: src/rotateinfo.c:103 src/rotateinfo.c:183 +msgid "Off" +msgstr "無効" + +#: src/system-prompt.c:373 +msgid "Passwords do not match." +msgstr "パスワードが一致しません。" + +#: src/system-prompt.c:380 +msgid "Password cannot be blank" +msgstr "パスワードを空欄にすることはできません" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "懐中電灯" + +#: src/ui/app-auth-prompt.ui:41 +msgid "Remember decision" +msgstr "覚える" + +#: src/ui/app-grid-button.ui:49 +msgid "App" +msgstr "アプリ" + +#: src/ui/app-grid-button.ui:76 +msgid "Remove from _Favorites" +msgstr "お気に入りから消す(_F)" + +#: src/ui/app-grid-button.ui:81 +msgid "Add to _Favorites" +msgstr "お気に入りに追加(_F)" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "アプリの検索" + +#: src/ui/lockscreen.ui:37 +msgid "Slide up to unlock" +msgstr "ロック解除" + +#: src/ui/lockscreen.ui:280 +msgid "Emergency" +msgstr "緊急" + +#: src/ui/lockscreen.ui:296 +msgid "Unlock" +msgstr "解除" + +#: src/ui/network-auth-prompt.ui:89 +msgid "_Cancel" +msgstr "キャンセル(_C)" + +#: src/ui/end-session-dialog.ui:32 +msgid "Some applications are busy or have unsaved work" +msgstr "使用中または未保存したデータでございます。" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:7 +#| msgid "Authenticate" +msgid "Authentication required" +msgstr "認証必須" + +#: src/ui/app-auth-prompt.ui:66 src/ui/end-session-dialog.ui:61 +msgid "Ok" +msgstr "OK" + +#: src/ui/network-auth-prompt.ui:105 +msgid "C_onnect" +msgstr "接続(_O)" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "ユーザー:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "確認:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "ロック画面" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "サインアウト" + +#: src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "再起動" + +#: src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "シャットダウン" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "WiFi" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:170 +msgid "Cellular" +msgstr "モバイル通信" + +#: src/monitor-manager.c:130 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %sn" +msgstr "%s %sn" diff --git a/po/ka.po b/po/ka.po new file mode 100644 index 000000000..39a9ddfe3 --- /dev/null +++ b/po/ka.po @@ -0,0 +1,1322 @@ +# Georgian translation for phosh. +# Copyright (C) 2023, phosh authors +# This file is distributed under the same license as the phosh package. +# Ekaterine Papava , 2023-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-05 08:39+0100\n" +"Last-Translator: Ekaterine Papava \n" +"Language-Team: Georgian <(nothing)>\n" +"Language: ka\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "ტელეფონის შელი" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "მობილურზე ფანჯრების მართვა და აპლიკაციის გაშვება" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "ეს სესია Phosh-ში შეგიყვანთ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Caffeine-ის სწრაფი მორგება" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "სესიისთვის უქმეობის ხელის შეშლა" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "კალენდარი" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "მარტივი კალენდრის ვიჯეტი" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "ბნელი რეჟიმის / ფერების სქემის სწრაფი მორგება" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "მუქი რეჟიმის გადართვა" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "გადაუდებელი ინფორმაცია" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "გადაუდებელი ინფორმაციისა და კონტაქტების ჩვენება" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "გამშვების ფანჯარა" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"გამშვებების დამატება დაბლოკილ ეკრანზე ეს დამატება ექსპერიმენტულია." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "მდებარეობის სწრაფი მორგება" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "გეოლოკაციის სერვისების გადართვა" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "მედიადამკვრელები" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "ამჟამად გაშვებული მედიის დამკვრელების თვალყურის დევნება" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "მობილური ინტერნეტის სწრაფი ჩართ/გამორთ პარამეტრი" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "მობილური ინტერნეტის ჩართ/გამორთ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "ღამის ფერების სწრაფი მორგება" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "ღამის სინათლის გადართვა" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "პომოდოროს სწრაფი მორგება" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "მარტივი ტაიმერი პომოდორო" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "ბილეთის ყუთი" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"დაბლოკილ ეკრანზე PDF-ების ჩვენება. ეს დამატება ექსპერიმენტულია. ეს " +"დამატება ექსპერიმენტულია." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "უახლოესი მოვლენები" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "კალენდრის მომავალი მოვლენების ჩვენება" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "დამატება საქაღალდეში" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "ახალი საქაღალდის შექმნა" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "აპლიკაცია" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "ყველა აპლიკაციის ჩვენება" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "მხოლოდ მობილურზე მორგებული აპლიკაციების ჩვენება" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "ტელეფონის გარსის ხმის კონტროლი" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "ელემენტი %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "ჩართ" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "მიერთებადი ბლუთუზის მოწყობილობები აღმოჩენილი არაა" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "ბლუთუზი გამორთულია" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "უცნობი აბონენტი" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wifi ქსელი '%s' ავტორიზაციის პორტალს იყენებს" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "WiFi ქსელი ავტორიზაციის პორტალს იყენებს" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "შესვლა Wi-Fi ქსელში" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "მიმაგრებული" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "მომძვრალი" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "დიახ" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "გადაუდებელი ზარის განხორციელება შეუძლებელია" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "შიდა შეცდომა" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "გამოსვლა" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s გამოვა ავტომატურად, %d წამში." +msgstr[1] "%s გამოვა ავტომატურად, %d წამში." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "გამორთვა" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "სისტემა გამოირთვება ავტომატურად %d წამში." +msgstr[1] "სისტემა გამოირთვება ავტომატურად %d წამში." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "გადატვრთვა" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "სისტემა გადაიტვირთება ავტომატურად %d წამში." +msgstr[1] "სისტემა გადაიტვირთება ავტომატურად %d წამში." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "მშვიდი" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "ჩუმი" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "ჩართ" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "მივცეთ უფლება %s-ს, მიიღოს ინფორმაცია თქვენი მდებარეობის შესახებ?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "გეოლოკაცია" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "დიახ" + +#: src/location-manager.c:272 +msgid "No" +msgstr "არა" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "შეიყვანეთ კოდი" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "შემოწმება…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "ეკრანის ანაბეჭდი შენახულია ფაილში '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "ეკრანის ანაბეჭდის შენახვა ვერ მოხერხდა" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "ეკრანის ანაბეჭდი" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "ეკრანის ანაბეჭდები" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "ეკრანის ანაბეჭდი %s%s.png-დან" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "უცნობი სათაური" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "უცნობი შემსრულებელი" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "ჩაშენებული ეკრანი" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "უცნობი" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Wi-Fi ქსელ %s-ის ავტენტიკაციის ტიპი მხარდაჭერილი არაა" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "შეიყვანეთ პაროლი Wi-Fi ქსელისთვის \"%s\"" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "გახსნა" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "გაფრთხილება" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "ახლა" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30წმ" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1წთ" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1წთ" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dწთ" +msgstr[1] "%dწთ" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dსთ" +msgstr[1] "~%dსთ" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1დ" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dდღე" +msgstr[1] "%dდღე" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1თვე" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dთვე" +msgstr[1] "%dთვე" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dწ" +msgstr[1] "~%dწ" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%d წელზე მეტი" +msgstr[1] "%d წელზე მეტი" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "თითქმის %dწ" +msgstr[1] "თითქმის %dწ" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "ავთენტიკაციის ფანჯარა დახურულია მომხმარებლის მიერ" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "პაროლი:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "უკაცრავად, არ იმუშავა. კიდევ სცადეთ." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "პორტრეტი" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "ლანდშაფტი" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "გამორთ" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "ჩართ" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "დასახურად დააწექით ESC-ს" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "%s-ის გაშვების შეცდომა" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "პაროლები არ ემთხვევა." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "პაროლები ცარიელი ვერ იქნება" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "შეხება" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "გადაწყვეტილების დამახსოვრება" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "გაუქმება" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "რჩეუ_ლებიდან წაშლა" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "_რჩეულებში დამატება" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "დეტალების _ნახვა" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "წაშლა" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_საქაღალდიდან წაშლა" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "აპპების ძებნა…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "გამოტანის მოწყობილობები" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "შეტანის მოწყობილობები" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "ხმის მორგება" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "ავტომატური სიკაშკაშე" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "სიკაშკაშის მორგება" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "ბლუთუზის ჩართვა" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "ბლუთუზის პარამეტრები" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "გადაუდებელი ზარის ფანჯრის დახურვა" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "გადაუდებელი _კონტაქტები" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "გადაუდებელი კონტაქტების გვერდზე გადასვლა" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "საგანგებო ციფერბლატის გვერდზე დაბრუნება" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "მფლობელი უცნობია" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "გადაუდებელი კონტაქტები" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "გადაუდებელი კონტაქტების გარეშე." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "ზოგიერთი აპლიკაცია დაკავებულია ან აქვს შეუნახავი ცვლილებები" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "უკუკავშირი" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "არ შემაწუხო" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "უკუკავშირის მორგება" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "მომხმარებელი:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "დომენი:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "და_კავშირება" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "უკან" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "აქაჩეთ გასახსნელად" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "გახსნა" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "საჭროა ავთენტიკაცია" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_გაუქმება" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_კავშირი" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "ავთენტიკაცია" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "ძილი" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "ჩაკეტვა" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "სასწრაფო" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "ბრძანების გაშვება" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "გაფრთხილების გარეშე" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "გაფრთხილებები" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "ყველას გასუფთავება" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "დასტური:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_გამორთვა…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_გადატვირთვა…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_ძილი…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "გა_სვლა…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi-ის მორგება" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "დამატება ვერ ვიპოვე" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "დამატება არ ჩაიტვირთულა: \"%s\"." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Wi-Fi მოწყობილობა ვერ ვიპოვე" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi გამორთულია" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Wi-Fi-ის ჩართვა" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi ჰოტსპოტი აქტიურია" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "გამორთვა" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Wi-Fi ჰოტსპოტების გარეშე" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "მობილური" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh caffeine-ზე" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "გამორთ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "ჩართ" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Caffeine-ის ტაიმერები" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "კოფეინის შუალედების გარეშე" + +#: plugins/caffeine-quick-setting/qs.ui:55 +#| msgid "Caffeine Quick Setting" +msgid "Caffeine Settings" +msgstr "Caffeine-iის მორგება" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "უვადო (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Caffeine-ის სწრაფი მორგების პარამეტრები" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "კოფეინის ხანგრძლივობა" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "კოფეინის ხანგრძლივობის მართვა" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "მომხმარებლის კოფეინის შუალედების დამატება ან წაშლა" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "ინტერვალის დამატება" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "ახალი ინტერვალის დამატება" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_დამატება" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "სწრაფი გაშვების ინტერვალები" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 წთ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 წთ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 წთ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 სთ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "აირჩიეთ ინტერვალი" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "ნაგულისხმევი სტილი" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "მუქი რეჟიმი" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "ღია რეჟიმი" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "პირადი ინფორმაცია" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "დაბადების თარიღი" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "სასურველი ენა" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "სახლის მისამართი" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "სამედიცინო ინფორმაცია" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "ასაკი" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "სისხლის ტიპი" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "სიმაღლე" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "სიმძიმე" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "ალერგიები" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "წამლები & პირობები" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Სხვა ინფორმაცია" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "გადაუდებელი ინფორმაციის მორგება" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "დასრულებულია" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "მფლ_ობელის სახელი" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_დაბადების თარიღი" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_სასურველი ენა" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_ასაკი" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_სისხლის ტიპი" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "სიმაღლ_ე" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_წონა" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "წამლები & პირობები" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "კონტაქტის დამატება" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "ახალი კონტაქტის დამატება" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_კონტაქტის სახელი" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "ურთიერთობები" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_კონტაქტის ნომერი" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "გამშვებები მორგებული არაა" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "გამშვებები" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "გეოლოკაცია ჩართულია" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "გეოლოკაცია გამორთულია" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "გაშვებული მედიადამკვრელების გარეშე" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "მობილური ინტერნეტი ჩართ" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "მობილური ინტერნეტი გამორთ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "ღამის ფერები ჩართულია" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "ღამის ფერები გათიშულია" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "პომოდოროს დაწყება" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "ფოკუსი თქვენს დავალებაზე %d წუთით" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "დასვენების აღება" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "გაქვთ %d წუთი შემდეგ პომოდორომდე" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "პომოდოროს ტაიმერი" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "პომოდორო გამორთულია" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "პომოდოროს სწრაფი მორგება" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "პომოდოროს ტექნიკა" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_აქტიურობის ხანგრძლივობა" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "ფოკუსის სესიის ხანგრძლივობა" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "შესვენე_ბის ხანგრძლივობა" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "სესიებს შორის შესვენების ხანგრძლივობა" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "გაშვება განბ_ლოკვისას" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "გაეშვება თუ არა ტაიმერი ეკრანის განბლოკვისას" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "საჩვენებელი დოკუმენტების გარეშე" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "ბილეთები" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "გახსნა" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "აირჩიეთ საქაღალდე" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "ბილეთის ყუთის მორგება" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "კონტურები" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "საქაღალდის მორგება" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "სად მოძებნის Phosh თქვენს ბილეთებს" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "ბილეთის საქაღალდე" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "დღეს" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "ხვალ" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "მოვლენების გარეშე" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "მთელი დღე" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "მთავრდება" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "უსახელო მოვლენა" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "მოვლენების გარეშე შემდეგი %d დღე" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "უახლოეს მოვალში მოვლენები არ არის" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "უახლოესი მოვლენების პარამეტრები" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "დღე" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "პერიოდი" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "დღეების რაოდენობა მოვლენების საჩვენებლად" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "ეკრანის მასშტაბები" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Wi-Fi ჰოტსპოტი" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "ჩართვა" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "ჰოტსპოტი ჩართ" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "ჰოტსპოტი გამორთ" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "%u დღეში" + +#~ msgid "Add" +#~ msgstr "დამატება" + +#~ msgid "Number" +#~ msgstr "რიცხვი" + +#~ msgid "Scan" +#~ msgstr "სკანირება" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "წმ" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "წთ" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "წთ" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "სთ" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "სთ" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "დღ" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "დღ" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "თვ" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "თვ" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "წლ" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "წლ" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "აპპი" + +#~ msgid "_Power Off" +#~ msgstr "_გამორთვა" + +#~ msgid "_Screenshot" +#~ msgstr "_ეკრანის ანაბეჭდი" + +#~ msgid "Unknown application" +#~ msgstr "უცნობი აპლიკაცია" + +#~ msgid "Lock Screen" +#~ msgstr "ეკრანის ჩაკეტვა" + +#~ msgid "Logout" +#~ msgstr "გასვლა" + +#, c-format +#~ msgid "On %A" +#~ msgstr "%A-ზე" diff --git a/po/ko.po b/po/ko.po new file mode 100644 index 000000000..dcac090fa --- /dev/null +++ b/po/ko.po @@ -0,0 +1,288 @@ +# Korean translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Seong-ho Cho , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/phosh/issues\n" +"POT-Creation-Date: 2020-09-13 03:31+0000\n" +"PO-Revision-Date: 2020-09-13 16:02+0900\n" +"Language-Team: Korean \n" +"Language: ko\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"Last-Translator: Seong-ho Cho \n" +"X-Generator: Poedit 2.3.1\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "폰 셸" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "모바일에서 창을 관리하고 프로그램을 실행합니다" + +#: src/app-grid-button.c:530 +msgid "Application" +msgstr "프로그램" + +#: src/bt-info.c:89 src/feedbackinfo.c:48 +msgid "On" +msgstr "켬" + +#: src/bt-info.c:91 +msgid "Bluetooth" +msgstr "블루투스" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:41 +msgid "Quiet" +msgstr "무음+진동" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:46 +msgid "Silent" +msgstr "무음+무진동" + +#: src/lockscreen.c:84 src/ui/lockscreen.ui:234 +msgid "Enter Passcode" +msgstr "패스코드 입력" + +#: src/lockscreen.c:263 +msgid "Checking…" +msgstr "검사중…" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:341 +msgid "%A, %B %-e" +msgstr "%B %d일 %A" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:263 src/ui/media-player.ui:107 +msgid "Unknown Title" +msgstr "알 수 없는 제목" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:272 src/ui/media-player.ui:127 +msgid "Unknown Artist" +msgstr "알 수 없는 작가" + +#: src/monitor-manager.c:58 +msgid "Built-in display" +msgstr "내장 디스플레이" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:62 +msgid "Unknown" +msgstr "알 수 없음" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "“%s” 무선 네트워크 인증 형식을 지원하지 않습니다" + +#: src/network-auth-prompt.c:189 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "“%s” 무선 네트워크 암호를 입력하십시오" + +#: src/notifications/notification.c:382 src/notifications/notification.c:601 +msgid "Notification" +msgstr "알림" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "초" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "분" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "분" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "시" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "시간" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "일" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "일" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "월" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "월" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "년" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "년" + +#. Translators: this is the date in (short) number only format +#: src/notifications/timestamp-label.c:107 +msgid "%d.%m.%y" +msgstr "%y.%m.%d" + +#. Translators: Timestamp prefix (e.g. Over 5h) +#: src/notifications/timestamp-label.c:198 +msgid "Over" +msgstr "지남: " + +#. Translators: Timestamp prefix (e.g. Almost 5h) +#: src/notifications/timestamp-label.c:203 +msgid "Almost" +msgstr "직전: " + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "사용자가 인증 대화상자를 닫았습니다" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "암호:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "죄송하지만, 동작하지 않습니다. 다시 시도하십시오." + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "인증" + +#: src/rotateinfo.c:46 +msgid "Portrait" +msgstr "세로" + +#: src/rotateinfo.c:49 +msgid "Landscape" +msgstr "가로" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "암호가 일치하지 않습니다." + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "암호를 비워둘 수 없습니다" + +#: src/ui/app-grid-button.ui:49 +msgid "App" +msgstr "프로그램" + +#: src/ui/app-grid-button.ui:76 +msgid "Remove from _Favorites" +msgstr "즐겨찾기에서 제거(_F)" + +#: src/ui/app-grid-button.ui:81 +msgid "Add to _Favorites" +msgstr "즐겨찾기에 추가(_F)" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "프로그램 검색…" + +#: src/ui/lockscreen.ui:37 +msgid "Slide up to unlock" +msgstr "끌어 올려서 잠금 해제" + +#: src/ui/lockscreen.ui:280 +msgid "Emergency" +msgstr "긴급 통화" + +#: src/ui/lockscreen.ui:296 +msgid "Unlock" +msgstr "잠금 해제" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "취소(_C)" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "연결(_O)" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "사용자:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "확인:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "잠금 화면" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "로그아웃" + +#: src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "다시 시작" + +#: src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "전원 끄기" + +#: src/wifiinfo.c:88 +msgid "Wi-Fi" +msgstr "무선랜" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:168 +msgid "Cellular" +msgstr "셀룰라" diff --git a/po/kw.po b/po/kw.po new file mode 100644 index 000000000..fdb043b27 --- /dev/null +++ b/po/kw.po @@ -0,0 +1,1243 @@ +# Cornish translation for phosh. +# Copyright (C) 2026 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# FIRST AUTHOR , YEAR. +# Flynn Peck , 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-08 22:24+0000\n" +"Last-Translator: Flynn Peck \n" +"Language-Team: kw\n" +"Language: kw\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-DL-VCS-Web: https://gitlab.gnome.org/World/Phosh/phosh\n" +"X-DL-Lang: kw\n" +"X-DL-Module: phosh\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-State: Translating\n" +"X-Generator: Gtranslator 49.0\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Shell Kellgowser" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Dyghtyans fenestri ha lonchyans appys rag kellgowsoryon" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "An esedhek ma omgelmi ty yn Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Settyans Uskis Kaffin" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Lettya an esedhek rag bos diek" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalender" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Unn kalender sempel" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Settyans Uskis Fordh Du / Gis" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Dewis fordh du" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Kedhlow Goredhom" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Diskwedhes kedhlow goredhom ha kestavow" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Boks Lonchyer" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Addya lonchyoryon war an alhwedh-skrin. An ystynnans ma yw arbrovel." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Settyans Uskis GPS" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Dewis GPS byw/marow" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Gwarioryon Media" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Helerghi gwarioryon media a'n jydh" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Settyans Uskis Data Kellgowser" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Dewis data kellgowser byw/marow" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Settyansow Uskis Golow Nos" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Dewis golow nos byw/marow" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Settyans Uskis Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Termynadow Sempel Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Boks Tokyn" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Diskwedhes PDFs war an alhwedh-skrin. An ystynnans ma yw arbrovel." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Hwarvosow a Dheu" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Diskwedhes hwarvosow kalender a dheu" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Addya dhe Restrenva" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Gul restrenva nowydh" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "App" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Diskwedhes Oll Appys" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Diskwedhes Appys Kellgowser-Kowethek Hepken" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Routyans Ughelder" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Batri %.0F%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Byw" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Devisyow Bluetooth junyadow kavosys vyth" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth marow" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Galowyer ankoth" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Rosweyth Wi-Fi '%s' devnydhya unn portal keth" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "An rosweyth Wi-Fi devnydhya unn portal keth" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Omgelmi yn rosweyth Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Junys" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Anjunys" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Da lowr" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Anabel dhe gelwel galow goredhom" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Error pervedhek" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Omdhigelmi" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s a wra omdhigelmys yn awtomatek yn %d eylen." +msgstr[1] "%s a wra omdhigelmys yn awtomatek yn %d eylennow." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Marowhe" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "An system a wra marowhe yn awtomatek yn %d eylen." +msgstr[1] "An system a wra marowhe yn awtomatek yn %d eylennow." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Dastalleth" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "An system a wra dastalleth yn awtomatek yn %d eylen." +msgstr[1] "An system a wra dastalleth yn awtomatek yn %d eylennow." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Tawel" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Tawesek" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Byw" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Gasa '%s' drehedhes dha kedhlow GPS?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "GPS" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Ea" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Na" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Ynworra Kod-Tremena" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Ow Checkya…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Skrin-imach sawys dhe '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Y fyllis sawya skrin-imach" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Skrin-Imach" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Skrin-Imajys" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Skrin-Imach dhyworth %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Titel Ankoth" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Awtour Ankoth" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Skrin pervedhek" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Ankoth" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Eghen omgelmyans a rosweyth Wi-Fi “%s” anskoodhys" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Ynworra ger-tremena rag an rosweyth Wi-Fi “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Ygeri" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Gwarnyans" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "lemmyn" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30e" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%deur" +msgstr[1] "~%deur" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mis" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmis" +msgstr[1] "%dmis" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%db" +msgstr[1] "~%db" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Moy es %db" +msgstr[1] "Moy es %db" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Ogas %db" +msgstr[1] "Ogas %db" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Fenester omgelmyans yw naghys gans an usyer" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Ger-tremena:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Drog yw genev, na na oberi. Mar pleg assaya arta." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Plommwedhek" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Gorwelyek" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Marow" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Byw" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Tava ESC rag degea" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Owth eksekutya '%s' fyllis" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Geryow-tremena na omdhesedha." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Ger-tremena a res" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Torchen" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Kova ervirans" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Hedhi" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Remova rag _Drudh" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Addya dhe _Drudh" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Gweles _Manylyon" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Dilea" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Remova rag Restrenva" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Hwilas appys…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Devisyow Eskorrans" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Devisyow Ynworrans" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Settyansow Son" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Golewder Awtomatek" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Settyansow Golewder" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Gallosegi Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Settyansow Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Degea an fenester galow goredhom" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Kestavow Goredhom" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Mos dhe'n folen kestavow goredhom" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Dehweles dhe'n folen niverpad goredhom" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Perghen ankoth" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Kestavow Goredhom" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Kestavow goredhom vyth kavadow." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Nebes appys yw bysi po kavos ober ansawys" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Dasliv" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Na ankresya" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Settyansow Dasliv" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Usyer:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Arlotteth:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Ju_nya" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "A-Dhelergh" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Skubya war-vann rag dialhwedha" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Dialhwedha" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Omgelmyans a res" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Hedhi" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Junya" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Omgelmi" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Koska" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Alhwedha" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Goredhom" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Eksekutya Arghadow" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Gwarnyansow vyth" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Gwarnyansow" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Dilea oll" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Konfirmya:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Marowhe…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Dastalleth…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Koska…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Omdhigelmi…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Settyansow Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Ystynnans na kavosys" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "An ystynnans '%s' na gallos bos karga." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Devis Wi-Fi Kavosys Vyth" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi Marow" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Gallosegi Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Hotspot Wi-Fi Byw" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Marowhe" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Hotspotys Wi-Fi Vyth" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Data Kellgowser" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh war kaffin" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Marow" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Byw" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Termynador kaffin" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Spysow kaffin vyth" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Settyansow Kaffin" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Diwedhva vyth (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Settyansow Settyans Uskis Kaffin" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Termyn Kaffin" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Dyghtya Termyn Kaffin" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Addya po remova spysow kaffin a-vusur" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Addya spys" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Addya Spys Nowydh" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Addya" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Spysow Snell-Dalleth" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 eur" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Dewis Spys" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Gis defowt" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Fordh du" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Fordh gwynn" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Kedhlow Personel" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Dedhyas Dinythyans" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Yeth Dewisek" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Trigva Tre" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Kedhlow Medhegel" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Bloodh" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Eghen Goos" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Ughelder" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Poos" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergedhow" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medhegnethow & Studhow" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Kedhlow Aral" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Settyansow Kedhlow Goredhom" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Deu" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Hanow Perghen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Dedhyas Dinythyans" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Yeth Dewisek" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Bloodh" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Eghen Goos" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Ughelder" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Poos" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medhegnethow ha Studhow" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Addya Kestav" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Addya Kestav Nowydh" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Hanow _Kestav" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Keskolm" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Niver Kestav" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Lonchyoryon selys vyth" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Lonchyoryon" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "GPS Byw" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "GPS Marow" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Gwarioyon media ow seni vyth" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Data Kellgowser Byw" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Data Kellgowser Marow" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Golow Nos Byw" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Golow Nos Marow" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Dalleth pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Fogella war dha oberen rag %d mynysennow" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Kemeres unn powes" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Ty kavos %d mynysennow bys yn Pomodoro nessa" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Termynador Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Marow" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Settyansow Settyans Uskis Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Teknek Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Termyn _Byw" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Termyn an esedhek fog" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Termyn _Powes" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Termyn an powes tredh esedhek" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Dalleth dres dialhwedha" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Mar dalleth an termynador dres skrin-dialhwedha" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Skrifow vyth rag displetyans" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Toknys" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Ygeri" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Dewis Restrenva" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Settyansow Boks Tokyn" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Tylleryow" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Settyansow Restrenva" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Pan Phosh hwilas rag dha toknys" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Restrenva Toknys" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hedhyw" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "A-Vorow" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Hwarvosow vyth" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Dres an jydh" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Finsya" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Hwarvos antitlys" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Hwarvosow vyth rag an nessa %d dedhyow" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Hwarvosow tost vyth" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Settyansow Hwarvosow Tost" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dedhyow" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Efander Dydh" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Niver dedhyow rag diskwedhes hwarvosow" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Brasterow skrin" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Hotspot Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Bywhe" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Hotspot Byw" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Hotspot Marow" diff --git a/po/la.po b/po/la.po new file mode 100644 index 000000000..6e3c0bec8 --- /dev/null +++ b/po/la.po @@ -0,0 +1,146 @@ +# Josh , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-14 13:18+0100\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-11-04 03:31+0000\n" +"Last-Translator: Josh \n" +"Language-Team: Latin\n" +"Language: la\n" +"X-Generator: Zanata 4.6.2\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Terminale Telephonicus" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "" +"Administratio fenestrarum et pellere programmarum pro machinatione mobile" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: src/app-grid-button.c:523 +msgid "Application" +msgstr "Programma" + +#: src/feedbackinfo.c:38 +msgid "Quiet" +msgstr "" + +#: src/feedbackinfo.c:40 +msgid "Silent" +msgstr "" + +#: src/feedbackinfo.c:42 +msgid "On" +msgstr "" + +#: src/lockscreen.c:78 src/ui/lockscreen.ui:204 +msgid "Enter Passcode" +msgstr "Induce signum" + +#: src/lockscreen.c:257 +msgid "Checking…" +msgstr "Verificans..." + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:334 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/monitor-manager.c:53 +msgid "Built-in display" +msgstr "Scrinium insitum" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:57 +msgid "Unknown" +msgstr "Incognitus" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "" + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "Fenestra authenticandi demissa est ab actore" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "Signum:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Me paenitet, ille non functionalis erat. Quaeso tempti iterum." + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "Authenticare" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "Signa non paria." + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "Noli esse vacuum signum" + +#: src/wifiinfo.c:55 +msgid "Wi-Fi" +msgstr "" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "Programma" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Inquirere programmas..." + +#: src/ui/lockscreen.ui:36 +msgid "Slide up to unlock" +msgstr "Trahe susum disserere" + +#: src/ui/lockscreen.ui:250 +msgid "Emergency" +msgstr "" + +#: src/ui/lockscreen.ui:266 +msgid "Unlock" +msgstr "" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "Actor:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "Confirmi:" diff --git a/po/lv.po b/po/lv.po new file mode 100644 index 000000000..93524a346 --- /dev/null +++ b/po/lv.po @@ -0,0 +1,327 @@ +# This is a Latvian translation file for Phosh. +# +# Krists Zeidlers , 2021. +# Rūdolfs Mazurs , 2021. +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/phosh/issues\n" +"POT-Creation-Date: 2021-02-13 15:33+0000\n" +"PO-Revision-Date: 2021-02-18 10:29+0200\n" +"Last-Translator: Rūdolfs Mazurs \n" +"Language-Team: Latvian \n" +"Language: lv_LV\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 :" +" 2);\n" +"X-Generator: Lokalize 19.12.3\n" + +#Ja kāds zin kā pareizi Phosh, Phone Shell utt. tulkot, tad lūdzu arī to izdariet. Es negribu dumjības šeit sarakstīt. +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Logu pārvaldība un lietotņu palaišana mobilajām iekārtām" + +#: src/app-grid-button.c:536 +msgid "Application" +msgstr "Lietotne" + +#: src/bt-info.c:92 src/feedbackinfo.c:51 +msgid "On" +msgstr "Ieslēgts" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Dokots" + +#: src/docked-info.c:81 src/docked-info.c:195 +msgid "Undocked" +msgstr "Atdokots" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:44 +msgid "Quiet" +msgstr "Kluss" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:49 +msgid "Silent" +msgstr "Klusums" + +#: src/lockscreen.c:85 src/ui/lockscreen.ui:234 +msgid "Enter Passcode" +msgstr "Ievadiet piekļuves kodu" + +#: src/lockscreen.c:264 +msgid "Checking…" +msgstr "Pārbauda…" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:342 +msgid "%A, %B %-e" +msgstr "%A, %-e. %B" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:277 src/ui/media-player.ui:107 +msgid "Unknown Title" +msgstr "Nezināms nosaukums" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:286 src/ui/media-player.ui:127 +msgid "Unknown Artist" +msgstr "Nezināms izpildītājs" + +#: src/monitor-manager.c:108 +msgid "Built-in display" +msgstr "Iebūvētais ekrāns" + +#: src/monitor-manager.c:126 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:133 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %sn" +msgstr "%s %sn" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:142 +msgid "Unknown" +msgstr "Nezināms" + +#: src/network-auth-prompt.c:187 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Nav atbalstīts wifi tīkla “%s” autentifikācijas tips" + +#: src/network-auth-prompt.c:192 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Ievadiet wifi tīkla “%s” paroli" + +#: src/notifications/mount-notification.c:137 +msgid "Open" +msgstr "Atvērt" + +#: src/notifications/notification.c:381 src/notifications/notification.c:637 +msgid "Notification" +msgstr "Paziņojums" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "h" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "h" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "mēn" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mēn" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "g" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "g" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "tagad" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Vairāk par %dg" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Gandrīz %dg" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:225 +msgid "Authentication dialog was dismissed by the user" +msgstr "Lietotājs noraidīja autentifikācijas dialoglodziņu" + +#: src/polkit-auth-prompt.c:278 src/ui/network-auth-prompt.ui:127 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "Parole:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Diemžēl tas nenostrādāja. Lūdzu, mēģiniet vēlreiz." + +#: src/polkit-auth-prompt.c:470 +msgid "Authenticate" +msgstr "Autentificēt" + +#: src/rotateinfo.c:65 +msgid "Portrait" +msgstr "Portrets" + +#: src/rotateinfo.c:68 +msgid "Landscape" +msgstr "Ainava" + +#: src/system-prompt.c:375 +msgid "Passwords do not match." +msgstr "Paroles nesakrīt." + +#: src/system-prompt.c:382 +msgid "Password cannot be blank" +msgstr "Parole nevar būt tukša" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lukturis" + +#: src/ui/app-grid-button.ui:49 +msgid "App" +msgstr "Lietotne" + +#: src/ui/app-grid-button.ui:76 +msgid "Remove from _Favorites" +msgstr "Izņemt no _izlases" + +#: src/ui/app-grid-button.ui:81 +msgid "Add to _Favorites" +msgstr "Pievienot _izlasei" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Meklēt lietotnes…" + +#: src/ui/lockscreen.ui:37 +msgid "Slide up to unlock" +msgstr "Velciet uz augšu, lai atbloķētu" + +#: src/ui/lockscreen.ui:280 +msgid "Emergency" +msgstr "Ārkārtas situācija" + +#: src/ui/lockscreen.ui:296 +msgid "Unlock" +msgstr "Atbloķēt" + +#: src/ui/network-auth-prompt.ui:89 +msgid "_Cancel" +msgstr "At_celt" + +#: src/ui/network-auth-prompt.ui:105 +msgid "C_onnect" +msgstr "Savien_oties" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "Lietotājs:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "Apstiprināt:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Bloķēt ekrānu" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Izrakstīties" + +#: src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Pārstartēt" + +#: src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Izslēgt" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:170 +msgid "Cellular" +msgstr "Mobilais tīkls" diff --git a/po/meson.build b/po/meson.build new file mode 100644 index 000000000..1139cdc0d --- /dev/null +++ b/po/meson.build @@ -0,0 +1,2 @@ +i18n = import('i18n') +i18n.gettext('phosh', preset: 'glib') diff --git a/po/nb.po b/po/nb.po new file mode 100644 index 000000000..23ffb83e8 --- /dev/null +++ b/po/nb.po @@ -0,0 +1,547 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2022-11-19 05:32+0000\n" +"PO-Revision-Date: 2022-11-19 21:16+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: nb\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.1.1\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/sm.puri.Phosh.desktop.in.in:4 +#, fuzzy +msgid "Phone Shell" +msgstr "Telefonskall" + +#: data/sm.puri.Phosh.desktop.in.in:5 +#, fuzzy +msgid "Window management and application launching for mobile" +msgstr "Vindusstyring og applikasjonsstart for mobiler" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalender" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "A simple calendar widget" +msgstr "Et enkel kalender miniprogram" + +# Vanskelig uten contex. +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +#, fuzzy +msgid "Ticket Box" +msgstr "Billett boks" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "" +"Show PDFs on the lock screen. This plugin is experimental. This plugin is " +"experimental." +msgstr "" +"Vis PDFer på låst skjerm. Dette programtillegget er eksperimentelt. Dette " +"programtillegget er eksperimentelt." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Kommende Arrangementer" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Vis kommende kalender arrangementer" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Applikasjoner" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Vis alle applikasjoner" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Bare Vis Mobilvennlige Applikasjoner" + +#: src/bt-info.c:92 src/feedbackinfo.c:78 src/rotateinfo.c:103 +msgid "On" +msgstr "På" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Blåtann" + +#: src/docked-info.c:81 +#, fuzzy +msgid "Docked" +msgstr "Forankret" + +#: src/docked-info.c:81 src/docked-info.c:199 +#, fuzzy +msgid "Undocked" +msgstr "Ikke forankret" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Log Ut" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s blir logget ut automatisk om %d sekunder." +msgstr[1] "%s blir logget ut automatisk om %d sekunder." + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Slå Av" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Systemet blir slått av automatisk om %d sekunder." +msgstr[1] "Systemet blir slått av automatisk om %d sekunder." + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Omstart" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Systemet starter automatisk på nytt om %d sekunder." +msgstr[1] "Systemet starter automatisk på nytt om %d sekunder." + +#: src/end-session-dialog.c:269 +msgid "Unknown application" +msgstr "Ukjent applikasjon" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:69 +msgid "Quiet" +msgstr "Stille" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:75 +msgid "Silent" +msgstr "Stille" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "La '%s' få tilgang til stedsinformasjonen din?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolokasjon" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Nei" + +#: src/lockscreen.c:169 src/ui/lockscreen.ui:232 +msgid "Enter Passcode" +msgstr "Skriv kode" + +#: src/lockscreen.c:393 +msgid "Checking…" +msgstr "Sjekker…" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:182 +msgid "Unknown Title" +msgstr "Ukjent Tittel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:165 +msgid "Unknown Artist" +msgstr "Ukjent Artist" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Innebygget skjerm" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Ukjent" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Autentiseringstypen for trådløst nettverk “%s” er ikke støttet" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Skriv passordet for det trådløse nettverket “%s”" + +#: src/notifications/mount-notification.c:122 +msgid "Open" +msgstr "Åpne" + +#: src/notifications/notification.c:383 src/notifications/notification.c:639 +msgid "Notification" +msgstr "Varsel" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "m" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "m" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "h" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "h" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "år" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "år" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "nå" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Over %dy" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Nesten %dy" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:228 +msgid "Authentication dialog was dismissed by the user" +msgstr "Autentiseringsdialogen ble avvist av brukeren" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Passord:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Beklager, det fungerte ikke. Vennligst forsøk på nytt." + +#: src/rotateinfo.c:81 +msgid "Portrait" +msgstr "Portrett" + +#: src/rotateinfo.c:84 +msgid "Landscape" +msgstr "Landskap" + +#. Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) +#. Translators: Automatic screen orientation is off (locked/disabled) +#: src/rotateinfo.c:103 src/rotateinfo.c:186 +msgid "Off" +msgstr "Av" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Trykk på ESC for å lukke" + +#: src/run-command-manager.c:94 +#, c-format +msgid "Running '%s' failed" +msgstr "Kjøring av '%s' feilet" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Passordene er ikke like." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Passordet kan ikke være tomt" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lommelykt" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Husk avgjørelse" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:53 +msgid "Cancel" +msgstr "Avbryt" + +#: src/ui/app-auth-prompt.ui:71 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Ok" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Applikasjon" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Fjern fra _Favoritter" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Legg til _Favoritter" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Se _Detaljer" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Søk etter applikasjoner…" + +#: src/ui/end-session-dialog.ui:31 +#, fuzzy +msgid "Some applications are busy or have unsaved work" +msgstr "Noen applikasjoner kjører eller har ikke lagret data" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Bruker:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Domene:" + +# hmm +#: src/ui/gtk-mount-prompt.ui:150 +#, fuzzy +msgid "Co_nnect" +msgstr "Ko_ble til" + +#: src/ui/lockscreen.ui:39 src/ui/lockscreen.ui:340 +msgid "Back" +msgstr "Tilbake" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Skyv opp for å låse opp" + +#: src/ui/lockscreen.ui:282 +msgid "Emergency" +msgstr "Nødsituasjon" + +#: src/ui/lockscreen.ui:298 +msgid "Unlock" +msgstr "Lås opp" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Autentisering kreves" + +#: src/ui/network-auth-prompt.ui:40 +msgid "_Cancel" +msgstr "_Avbryt" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "K_oble til" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Autentiser" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Kjør Kommando" + +#: src/ui/settings.ui:301 +msgid "No notifications" +msgstr "Ingen varsler" + +#: src/ui/settings.ui:342 +msgid "Clear all" +msgstr "Slett alt" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Bekreft:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Lås Skjerm" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Logg ut" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:345 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Programtillegg ikke funnet" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Programtillegget '%s' kunne ikke lastes." + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Trådløst Nettverk" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:200 +msgid "Cellular" +msgstr "Mobilt nett" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Ingen dokumenter å vise" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Billetter" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Idag" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "I morgen" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "Om %d dag" +msgstr[1] "Om %d dager" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Ingen arrangementer" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Hele dagen" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Avslutter" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Ikke navngitt arrangement" diff --git a/po/nl.po b/po/nl.po new file mode 100644 index 000000000..96f6dcc8b --- /dev/null +++ b/po/nl.po @@ -0,0 +1,1228 @@ +# Joachim Moernaut , 2018. #zanata +# Joachim Moernaut , 2019. #zanata +# Willem Sonke , 2019. #zanata +# Jan Jasper de Kroon , 2021-2022. +# Nathan Follens , 2021-2025. +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-10-12 14:32+0000\n" +"PO-Revision-Date: 2025-10-12 17:08+0200\n" +"Last-Translator: Nathan Follens \n" +"Language-Team: GNOME-NL https://matrix.to/#/#nl:gnome.org\n" +"Language: nl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Telefoonshell" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Vensterbeheer en opstarten van mobiele toepassingen" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Deze sessie meldt u aan bij Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Caffeïne – snelle instelling" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Voorkomt dat de sessie inactief wordt" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Agenda" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Een eenvoudige agendawidget" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Donkere modus / kleurenschema – snelle instelling" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Donkere modus aan/uit" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Noodinfo" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Toon noodinformatie en -contacten" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Startervak" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Voeg starters toe aan het vergrendelscherm. Dit is een experimentele plug-" +"in." + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Mediaspelers" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Volg welk mediaspelers momenteel actief zijn" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Mobiele gegevens – snelle instelling" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Mobiele gegevens aan/uit" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Nachtlicht – snelle instelling" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Nachtlicht aan/uit" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro – snelle instelling" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Eenvoudige pomodorotimer" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Ticketvak" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Toon PDF’s op het vergrendelscherm. Dit is een experimentele plug-in." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Aankomende afspraken" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Aankomende agenda-afspraken weergeven" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Toevoegen aan map" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Nieuwe map aanmaken" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Toepassing" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Alle apps weergeven" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Alleen mobielvriendelijke apps weergeven" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Accu %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Aan" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "" +"Geen Bluetooth-apparaten gevonden waarmee verbinding gemaakt kan worden" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth uitgeschakeld" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Onbekende beller" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wifinetwerk ‘%s’ gebruikt een aanmeldportaal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Het wifinetwerk gebruikt een aanmeldportaal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Aanmelden bij wifinetwerk" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "In dock" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Uit dock" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Oké" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Noodoproep mislukt" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Interne fout" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Uitloggen" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s wordt automatisch uitgelogd over %d seconde." +msgstr[1] "%s wordt automatisch uitgelogd over %d seconden." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Uitschakelen" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Het systeem wordt automatisch uitgeschakeld over %d seconde." +msgstr[1] "Het systeem wordt automatisch uitgeschakeld over %d seconden." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Herstarten" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Het systeem zal automatisch herstarten over %d seconde." +msgstr[1] "Het systeem zal automatisch herstarten over %d seconden." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Rustig" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Stil" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Aan" + +#: src/location-manager.c:269 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "‘%s’ toegang geven tot uw locatiegegevens?" + +#: src/location-manager.c:274 +msgid "Geolocation" +msgstr "Geolocatie" + +#: src/location-manager.c:275 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:275 +msgid "No" +msgstr "Nee" + +#. give visual feedback on error +#: src/lockscreen.c:313 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Voer de pincode in" + +#: src/lockscreen.c:956 +msgid "Checking…" +msgstr "Bezig met controleren…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Schermafdruk opgeslagen als ‘%s’" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Kon schermafdruk niet opslaan" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Schermafdruk" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Schermafdrukken" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Schermafdruk opgeslagen als %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:646 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Onbekende titel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:654 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Onbekende artiest" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "Ingebouwd beeldscherm" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "Onbekend" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Authenticatietype van wifinetwerk ‘%s’ wordt niet ondersteund" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Voer het wachtwoord in voor wifinetwerk ‘%s’" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Openen" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1012 +msgid "Notification" +msgstr "Notificatie" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "nu" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%du" +msgstr[1] "~%du" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mnd" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmnd" +msgstr[1] "%dmnd" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dj" +msgstr[1] "~%dj" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Meer dan %dj" +msgstr[1] "Meer dan %dj" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Bijna %dj" +msgstr[1] "Bijna %dj" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Authenticatievenster is door de gebruiker afgesloten" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Wachtwoord:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Helaas, dat werkte niet. Probeer het opnieuw." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Staand" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Liggend" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Uit" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Aan" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Druk op ESC om te sluiten" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Uitvoeren van ‘%s’ mislukt" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Volumeregeling van Telefoonshell" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Wachtwoorden komen niet overeen." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Wachtwoord mag niet leeg zijn" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Zaklamp" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Keuze onthouden" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Annuleren" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Verwijderen uit _favorieten" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Toevoegen aan _favorieten" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "_Details bekijken" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Verwijderen" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Verwijde_ren uit map" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Apps zoeken…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Uitvoerapparaten" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Invoerapparaten" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Geluidsinstellingen" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Bluetooth inschakelen" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth-instellingen" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Sluit de noodoproepdialoog" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Nood_contacten" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Ga naar de noodcontactenpagina" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Ga terug naar het noodtoetsenblok" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Onbekende eigenaar" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Noodcontacten" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Geen noodcontacten beschikbaar." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Sommige applicaties zijn bezig of hebben niet-opgeslagen werk" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Feedback" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Niet storen" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "Feedbackinstellingen" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Gebruikersnaam:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domein:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Verbi_nden" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Terug" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Veeg omhoog om te ontgrendelen" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Ontgrendelen" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Verificatie vereist" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Annuleren" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Verbinden" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "Aanmelden" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Pauzestand" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Vergrendelen" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Noodgeval" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Opdracht uitvoeren" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Geen meldingen" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "Notificaties" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Alles wissen" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Bevestigen:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Uitschakelen…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "He_rstarten…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Slaapstand…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Afme_lden…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wifi" + +#: src/ui/wifi-status-page.ui:89 +msgid "Wi-Fi Settings" +msgstr "Wifi-instellingen" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A %d %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Plug-in niet gevonden" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "De plug-in ‘%s’ kon niet geladen worden." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Geen wifi-apparaat gevonden" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wifi uitgeschakeld" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Wifi inschakelen" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wifi-hotspot ingeschakeld" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Uitschakelen" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Geen wifi-hotspots" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobiel" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh met caffeïne" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Aan" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Uit" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Standaardstijl" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Donkere modus" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Lichte modus" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Persoonlijke informatie" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Geboortedatum" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Voorkeurstaal" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Thuisadres" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Medische informatie" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Leeftijd" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Bloedgroep" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Lengte" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Gewicht" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergieën" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medicatie & aandoeningen" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Overige informatie" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Voorkeuren voor noodinfo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Gereed" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Naam van _eigenaar" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Geboorte_datum" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Voorkeurstaal" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Leeftijd" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Bloedgroep" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Lengte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "Ge_wicht" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medicatie en aandoeningen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Contact toevoegen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Nieuw contact toevoegen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Toevoegen" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Contactnaam" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relatie" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Contactnummer" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Geen toepassingsstarter geconfigureerd" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Starters" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Geen mediaspelers actief" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:82 +msgid "Mobile Data On" +msgstr "Mobiele gegevens aan" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:82 +msgid "Mobile Data Off" +msgstr "Mobiele gegevens uit" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Nachtlicht aan" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Nachtlicht uit" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:75 +msgid "Pomodoro start" +msgstr "Pomodoro starten" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:76 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Focus %d minuten op uw taak" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:81 +msgid "Take a break" +msgstr "Neem even pauze" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:83 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "U hebt %d minuten tot de volgende pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:98 +msgid "Pomodoro Timer" +msgstr "Pomodorotimer" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:121 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro uit" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Voorkeuren voor Pomodoro – snelle instelling" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoromethode" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Activiteitsduur" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Duur van de focussessie" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Pauzeduur" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Duur van de pauze tussen sessies" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Starten bij ontgrendelen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Bepaalt of de timer moet starten wanneer het scherm ontgrendeld wordt" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Geen documenten om weer te geven" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Tickets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Openen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Selecteer map" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Voorkeuren voor ticketvak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Paden" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Mapinstellingen" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Waar Phosh uw tickets zoekt" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Ticketmap" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Vandaag" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Morgen" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Geen afspraken" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "De hele dag" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Loopt af" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Naamloze afspraak" + +#: plugins/upcoming-events/upcoming-events.c:372 +#, c-format +msgid "No events for the next %d days" +msgstr "Geen afspraken voor de volgende %d dagen" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Geen aankomende afspraken" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Voorkeuren voor aankomende afspraken" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dagen" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Datumbereik" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Aantal dagen waarvoor afspraken weergegeven moeten worden" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Beeldscherm schalen" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Hotspot aan" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Hotspot uit" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Schermafdruk gekopieerd naar klembord" + +#~ msgid "Add" +#~ msgstr "Toevoegen" + +#~ msgid "Number" +#~ msgstr "Nummer" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "Over %u dag" +#~ msgstr[1] "Over %u dagen" + +#~ msgid "Unknown application" +#~ msgstr "Onbekende applicatie" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "u" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "u" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mnd" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mnd" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "j" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "j" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "App" + +#~ msgid "_Power Off" +#~ msgstr "_Uitschakelen" + +#~ msgid "_Screenshot" +#~ msgstr "_Schermafdruk" + +#~ msgid "Lock Screen" +#~ msgstr "Vergrendelscherm" + +#~ msgid "Logout" +#~ msgstr "Afmelden" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Enkel adaptieve toepassingen tonen" + +#~ msgctxt "" +#~ "This is a monitor vendor name followed by product/model name where size " +#~ "in inches could not be calculated, e.g. Dell U2414H" +#~ msgid "%s %sn" +#~ msgstr "%s %sn" diff --git a/po/oc.po b/po/oc.po new file mode 100644 index 000000000..465c74e15 --- /dev/null +++ b/po/oc.po @@ -0,0 +1,1331 @@ +# Occitan translation for phosh. +# Copyright (C) 2022 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Quentin PAGÈS , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-10 17:02+0000\n" +"PO-Revision-Date: 2026-02-14 17:46+0100\n" +"Last-Translator: Quentin PAGÈS\n" +"Language-Team: Occitan \n" +"Language: oc\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Gestion de fenèstra e aviada d’aplicacions per mobil" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Agenda" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Bascular al mòde escur" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Informacions d'urgéncia" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Lectors multimèdia" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Apondre al dossièr" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Crear un dossièr novèl" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplicacion" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Afichar totas las aplicacions" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Afichar sonque las aplicacions per mobil" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Batariá %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +#, fuzzy +#| msgid "On" +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Alucat" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth desactivat" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Sonaire anonim" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Lo ret Wi-Fi « %s » utiliza un portal captiu" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Lo ret Wi-Fi utiliza un portal captiu" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "S’inscriure sul ret Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "D’acòrdi" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Error intèrna" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Se desconnectar" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s serà desconnectat automaticament d’aquí %d segonda." +msgstr[1] "%s serà desconnectat automaticament d’aquí %d segondas." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Atudar" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Lo sistèma s'atudarà automaticament d’aquí %d segonda." +msgstr[1] "Lo sistèma s'atudarà automaticament d’aquí %d segondas." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Reaviar" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Lo sistèma se reaviarà automaticament d’aquí %d segonda." +msgstr[1] "Lo sistèma se reaviarà automaticament d’aquí %d segondas." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Silenciós" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Mòde silenciós" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +#, fuzzy +#| msgid "On" +msgctxt "feedback:enabled" +msgid "On" +msgstr "Alucat" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Geolocalizacion" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Òc" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Non" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Verificacion…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Captura d'ecran" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Capturas d'ecran" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Títol desconegut" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Artista desconegut" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Afichatge integrat" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Desconegut" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Dobrir" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Notificacion" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "ara" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d min" +msgstr[1] "%d min" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 mes" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d mes" +msgstr[1] "%d meses" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d an" +msgstr[1] "~%d ans" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, fuzzy, c-format +#| msgid "Over %dy" +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Mai de %d ans" +msgstr[1] "Mai de %d ans" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Gaireben %d ans" +msgstr[1] "Gaireben %d ans" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Senhal :" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "" + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Retrach" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Païsatge" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +#, fuzzy +#| msgid "Off" +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Atudat" + +#: src/rotateinfo.c:126 +#, fuzzy +#| msgid "On" +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Alucat" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Quichatz Escap. per quitar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Los senhals correspondon pas." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Lo senhal pòt pas èsser void" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Lampa de pòcha" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Se remembrar de la causida" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Anullar" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Tirar dels _favorits" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Apondre als _favorits" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "_Veire los detalhs" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Desinstallar" + +#: src/ui/app-grid-button.ui:53 +#, fuzzy +#| msgid "Remove from _Favorites" +msgid "_Remove from Folder" +msgstr "Tirar dels _favorits" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Cercar aplicacions…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Periferics de sortida" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Periferics d'entrada" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Paramètres del son" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Luminositat automatica" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Paramètres de luminositat" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Activar lo Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Paramètres Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Contacte d’urgéncia" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Proprietari desconegut" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Contacte d’urgéncia" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "" + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "D'unas aplicacions son ocupadas o an de prèt pas salvats" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Destorbar pas" + +#: src/ui/feedback-status-page.ui:53 +#, fuzzy +#| msgid "Sound Settings" +msgid "Feedback Settings" +msgstr "Paramètres del son" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Utilizaire :" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domeni :" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Se co_nnectar" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Tornar" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Desverrolhar" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autentificacion requesida" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Anullar" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "Se _connectar" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "S'autentificar" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Metre en velha" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Verrolhar" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Urgéncia" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Executar la comanda" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Cap de notificacions" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Notificacions" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Tot escafar" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirmar :" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Atudar…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Reaviar…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Metre en velha…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Se _desconnectar…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Paramètres Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A %e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "" + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Cap de periferic Wi-Fi pas trobat" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi desactivat" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Activar lo Wifi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Punt d’accès Wi-Fi activat" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Atudar" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Cap de punt d’accès Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobil" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +#, fuzzy +#| msgid "Off" +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Atudat" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +#, fuzzy +#| msgid "On" +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Alucat" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "" + +#: plugins/caffeine-quick-setting/qs.ui:55 +#, fuzzy +#| msgid "Sound Settings" +msgid "Caffeine Settings" +msgstr "Paramètres del son" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Apondre un interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Apondre un interval novèl" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Apondre" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Causir l’interval" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Estil per defaut" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Mòde escur" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Mòde clar" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informacions personalas" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Data de naissença" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Lenga preferida" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Adreça personala" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informacions medicalas" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Atge" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Grop sanguin" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Talha" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Pes" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergias" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Tractaments e utilizacion" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Autras informacions" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferéncias d'informacion d'urgéncia" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Terminat" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Nom del proprietari" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Data de naissença" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Lenga preferida" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Atge" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Grop sanguin" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Nautor" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +#, fuzzy +#| msgid "Weight" +msgid "_Weight" +msgstr "Pes" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Apondre un contacte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Apondre contacte novèl" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Nom del contacte" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relacion" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Numèro del contacte" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Aviadors" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Geolocalizacion activa" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Geolocalizacion desactivada" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Donadas mobilas activas" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Donadas mobilas desactivas" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Mòde nuèch activat" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Mòde nuèch desactivat" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, fuzzy, c-format +#| msgid "Power Off" +msgid "Pomodoro Off" +msgstr "Atudar" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Tiquets" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Dobrir" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Causir un dossièr" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Uèi" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Deman" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Cap d’eveniment" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tota la jornada" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "S'acaba" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Eveniment sens títol" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "" + +#: plugins/upcoming-events/upcoming-events.ui:28 +#, fuzzy +#| msgid "No events" +msgid "No upcoming events" +msgstr "Cap d’eveniment" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +#, fuzzy +#| msgid "Emergency Info Preferences" +msgid "Upcoming Events Preferences" +msgstr "Preferéncias d'informacion d'urgéncia" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Jorns" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Punt d’accès Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "" + +#~ msgid "Add" +#~ msgstr "Apondre" + +#~ msgid "Number" +#~ msgstr "Numèro" + +#, c-format +#~ msgid "In %d day" +#~ msgid_plural "In %d days" +#~ msgstr[0] "D’aquí %d jorn" +#~ msgstr[1] "D’aquí %d jorns" + +#~ msgid "Unknown application" +#~ msgstr "Aplicacion desconeguda" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mes" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "meses" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "a" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "a" + +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplicacion" + +#~ msgid "Lock Screen" +#~ msgstr "Verrolhar l'ecran" + +#~ msgid "Logout" +#~ msgstr "Se desconnectar" diff --git a/po/pl.po b/po/pl.po new file mode 100644 index 000000000..5070fcd44 --- /dev/null +++ b/po/pl.po @@ -0,0 +1,773 @@ +# Polish translation for phosh. +# Copyright © 2022-2023 the phosh authors. +# This file is distributed under the same license as the phosh package. +# Piotr Drąg , 2022-2023. +# Aviary.pl , 2022-2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-07-05 08:48+0000\n" +"PO-Revision-Date: 2023-07-16 13:03+0200\n" +"Last-Translator: Piotr Drąg \n" +"Language-Team: Polish \n" +"Language: pl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 " +"|| n%100>=20) ? 1 : 2);\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Powłoka telefonu" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Zarządzanie oknami i uruchamianiem programów na telefonie" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Ta sesja loguje do powłoki Phosh" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Kalendarz" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Prosty widżet kalendarza" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Skrzynka na bilety" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Wyświetla dokumenty PDF na ekranie blokowania. Ta wtyczka jest " +"eksperymentalna." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Nadchodzące wydarzenia" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Wyświetlanie nadchodzących wydarzeń z kalendarza" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Program" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Wyświetl wszystkie programy" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Wyświetl tylko programy przystosowane do telefonów" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Włączone" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/call-notification.c:60 +msgid "Unknown caller" +msgstr "Nieznany numer" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Podłączony" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Niepodłączony" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Nie można wykonać połączenia alarmowego" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Błąd wewnętrzny" + +#: src/end-session-dialog.c:163 +msgid "Log Out" +msgstr "Wyloguj się" + +#: src/end-session-dialog.c:166 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "Użytkownik %s zostanie wylogowany za %d sekundę." +msgstr[1] "Użytkownik %s zostanie wylogowany za %d sekundy." +msgstr[2] "Użytkownik %s zostanie wylogowany za %d sekund." + +#: src/end-session-dialog.c:172 +msgid "Power Off" +msgstr "Wyłącz telefon" + +#: src/end-session-dialog.c:173 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Wyłączenie telefonu nastąpi za %d sekundę." +msgstr[1] "Wyłączenie telefonu nastąpi za %d sekundy." +msgstr[2] "Wyłączenie telefonu nastąpi za %d sekund." + +#: src/end-session-dialog.c:179 +msgid "Restart" +msgstr "Uruchom ponownie" + +#: src/end-session-dialog.c:180 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Ponowne uruchomienie telefonu nastąpi za %d sekundę." +msgstr[1] "Ponowne uruchomienie telefonu nastąpi za %d sekundy." +msgstr[2] "Ponowne uruchomienie telefonu nastąpi za %d sekund." + +#: src/end-session-dialog.c:270 +msgid "Unknown application" +msgstr "Nieznany program" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Cichy" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Wyciszony" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Włączone" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Zezwolić programowi „%s” na dostęp do położenia użytkownika?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Ustalanie położenia" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Tak" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Nie" + +#: src/lockscreen.c:174 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "Proszę podać kod" + +#: src/lockscreen.c:397 +msgid "Checking…" +msgstr "Sprawdzanie…" + +#: src/screenshot-manager.c:212 +msgid "Screenshot" +msgstr "Zrzut ekranu" + +#: src/screenshot-manager.c:213 +msgid "Screenshot copied to clipboard" +msgstr "Skopiowano zrzut ekranu do schowka" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:321 src/ui/media-player.ui:161 +msgid "Unknown Title" +msgstr "Nieznany tytuł" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:329 src/ui/media-player.ui:148 +msgid "Unknown Artist" +msgstr "Nieznany wykonawca" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Wbudowany ekran" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Nieznany" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Typ uwierzytelniania sieci Wi-Fi „%s” nie jest obsługiwany" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Proszę podać hasło sieci Wi-Fi „%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Otwórz" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "Powiadomienie" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "s" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "min" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "min" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "godz." + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "godz." + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "d" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "d" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "mies." + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "mies." + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "rok" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "lat." + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "teraz" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Ponad %d lat." + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Prawie %d lat." + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d %s" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "Okno uwierzytelniania zostało odrzucone przez użytkownika" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Hasło:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "To nie zadziałało. Proszę spróbować ponownie." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Pionowo" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Poziomo" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Wyłączone" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Włączone" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Klawisz Esc zamknie" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Wykonanie polecenia „%s” się nie powiodło" + +#: src/settings/audio-settings.c:373 +msgid "Phone Shell Volume Control" +msgstr "Sterowanie głośnością powłoki telefonu" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Hasła się nie zgadzają." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Hasło nie może być puste" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Latarka" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Zapamiętanie decyzji" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +msgid "Cancel" +msgstr "Anuluj" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Program" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "_Usuń z ulubionych" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Dodaj do _ulubionych" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Wyświetl _szczegóły" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Wyszukiwanie programów…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Urządzenia wyjściowe" + +#: src/ui/audio-settings.ui:107 +msgid "Input Devices" +msgstr "Urządzenia wejściowe" + +#: src/ui/audio-settings.ui:134 +msgid "Sound Settings" +msgstr "Ustawienia dźwięku" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Zamyka okno połączenia alarmowego" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Kontakty alarmowe" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Przechodzi do strony kontaktów alarmowych" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Wraca do strony klawiatury połączenia alarmowego" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Nieznany właściciel" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +msgid "Emergency Contacts" +msgstr "Kontakty alarmowe" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Brak dostępnych kontaktów alarmowych." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Niektóre programy są używane lub mają niezapisane dane" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Użytkownik:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Domena:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "_Połącz" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "Wstecz" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "Przesunięcie w górę odblokuje" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "Odblokuj" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Wymagane jest uwierzytelnienie" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "_Anuluj" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "_Połącz" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Uwierzytelnij" + +#: src/ui/power-menu.ui:69 +msgid "_Power Off" +msgstr "_Wyłącz telefon" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "Za_blokuj" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "Wykonaj zrzut _ekranu" + +#: src/ui/power-menu.ui:192 +msgid "_Emergency" +msgstr "Połączenie _alarmowe" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Wykonanie polecenia" + +#: src/ui/settings.ui:296 +msgid "No notifications" +msgstr "Brak powiadomień" + +#: src/ui/settings.ui:336 +msgid "Clear all" +msgstr "Wyczyść wszystkie" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Potwierdzenie:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "Wyłą_cz telefon…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "Uruchom po_nownie…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Uśpij…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Wyloguj się…" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +msgid "%A, %B %-e" +msgstr "%A, %-d %B" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Nie odnaleziono wtyczki" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Nie można wczytać wtyczki „%s”." + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Komórkowa" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Informacje osobiste" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Data urodzin" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Preferowany język" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Adres domowy" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "Informacje medyczne" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "Wiek" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "Grupa krwi" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "Wzrost" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "Waga" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "Alergie" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Leki i choroby" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "Inne informacje" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Preferencje informacji alarmowych" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Gotowe" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Imię i nazwisko właściciela" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "Leki i choroby" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "Dodaj kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "Dodanie nowego kontaktu" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "Dodaj" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:315 +msgid "New Contact Name" +msgstr "Imię i nazwisko nowego kontaktu" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:329 +msgid "Relationship" +msgstr "Pokrewieństwo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:342 +msgid "Number" +msgstr "Numer" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Brak dokumentów do wyświetlenia" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Bilety" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Wybór katalogu" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "_Otwórz" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferencje skrzynki na bilety" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Ścieżki" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Ustawienia katalogu" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Gdzie Phosh szuka biletów" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Katalog z biletami" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Dzisiaj" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Jutro" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "Za %d dzień" +msgstr[1] "Za %d dni" +msgstr[2] "Za %d dni" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Brak wydarzeń" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%H∶%M" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%-l∶%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Cały dzień" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Kończy się" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Wydarzenie bez tytułu" diff --git a/po/pt.po b/po/pt.po new file mode 100644 index 000000000..f97df755e --- /dev/null +++ b/po/pt.po @@ -0,0 +1,1118 @@ +# Portuguese translaiton to phosh. +# Copyright (C) 2021 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Cristovão Osório de Aragão Gomes Ferreira , 2019. #zanata +# Hugo Carvalho , 2020-2024. +# Juliano de Souza Camargo , 2020-2021. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2024-12-20 13:45+0000\n" +"PO-Revision-Date: 2024-12-23 21:53+0000\n" +"Last-Translator: Hugo Carvalho \n" +"Language-Team: Portuguese < https://l10n.gnome.org/teams/pt/>\n" +"Language: pt\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.5\n" +"X-DL-Team: pt\n" +"X-DL-Module: phosh\n" +"X-DL-Branch: main\n" +"X-DL-Domain: po\n" +"X-DL-State: None\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Interface de telefone" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Gestão de janelas e lançador de aplicações para telemóvel" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Esta sessão inicia-o no Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Definições rápidas Cafeína" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Evita que a sessão fique inativa" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Calendário" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Um simples widget de calendário" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Modo escuro / Definições rápidas do esquema de cores" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Alternar o modo escuro" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Informação de emergência" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Mostra informação de emergência e contatos" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Caixa de lançadores" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Adiciona lançadores na ecrã de bloqueio. Esta extensão é experimental." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Definições rápidas dos dados móveis" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Alterna os dados móveis" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Definições rápidas da luz noturna" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Alterna luz noturna" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Definições rápidas do Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Temporizador simples Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Bilheteira" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Mostrar PDFs no ecrã bloqueado. Este plugin é experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Próximos eventos" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Mostrar os próximos eventos do calendário" + +#: src/app-grid-button.c:153 +msgid "Add to Folder" +msgstr "Adicionar à pasta" + +#: src/app-grid-button.c:177 +msgid "Create new folder" +msgstr "Criar uma nova pasta" + +#: src/app-grid-button.c:692 src/app-grid-button.c:749 +msgid "Application" +msgstr "Aplicação" + +#: src/app-grid.c:263 +msgid "Show All Apps" +msgstr "Mostrar todas as aplicações" + +#: src/app-grid.c:266 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostrar apenas as aplicações conhecidas" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:72 +#, c-format +msgid "Battery %.0f%%" +msgstr "Bateria %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Ligado" + +#: src/bt-status-page.c:95 +msgid "No connectable Bluetooth Devices found" +msgstr "Sem dispositivos Bluetooth conectáveis" + +#: src/bt-status-page.c:99 +msgid "Bluetooth disabled" +msgstr "Bluetooth desativado" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Número desconhecido" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Ancorado" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desancorado" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "Aceitar" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Impossível colocar chamada de emergência" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Erro interno" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Sair" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s terminará automaticamente a sessão em %d segundo." +msgstr[1] "%s terminará automaticamente a sessão em %d segundos." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Desligar" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "O sistema desligará automaticamente dentro de %d segundo." +msgstr[1] "O sistema desligará automaticamente dentro de %d segundos." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Reiniciar" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "O sistema será reiniciado automaticamente dentro de %d segundo." +msgstr[1] "O sistema será reiniciado automaticamente dentro de %d segundos." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Silencioso" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silenciar" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Ligado" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Permitir que '%s' aceda à sua informação de localização?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolocalização" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Sim" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Não" + +#. give visual feedback on error +#: src/lockscreen.c:293 src/ui/lockscreen.ui:290 +msgid "Enter Passcode" +msgstr "Introduza o código" + +#: src/lockscreen.c:949 +msgid "Checking…" +msgstr "A verificar…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:235 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Captura de ecrã guardada em '%s'" + +#: src/screenshot-manager.c:237 +msgid "Failed to save screenshot" +msgstr "Falha ao guardar a captura de ecrã" + +#: src/screenshot-manager.c:241 src/ui/power-menu.ui:192 +msgid "Screenshot" +msgstr "Captura de ecrã" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:416 +msgid "Screenshots" +msgstr "Capturas de ecrã" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:436 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Captura de ecrã de %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:446 src/ui/media-player.ui:213 +msgid "Unknown Title" +msgstr "Título desconhecido" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:454 src/ui/media-player.ui:201 +msgid "Unknown Artist" +msgstr "Artista desconhecido" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "Ecrã integrado" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Desconhecido" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Tipo de autenticação da rede wifi “%s” sem suporte" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Insira a palavra-passe para a rede wifi “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Abrir" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:404 src/notifications/notification.c:677 +#: src/notifications/notify-manager.c:1004 +msgid "Notification" +msgstr "Notificações" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:86 +msgid "now" +msgstr "agora" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:91 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:96 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:101 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:109 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:117 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:122 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:127 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:132 +#, c-format +msgid "~1mo" +msgstr "~1mo" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:137 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmo" +msgstr[1] "%dmos" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:147 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:151 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Mais de %dy" +msgstr[1] "Mais de %dy" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:156 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Quase %dy" +msgstr[1] "Quase %dy" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "A janela de autenticação foi fechada pelo utilizador" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Palavra-passe:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Ups! Não funcionou. Tente novamente." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Retrato" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Paisagem" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desligada" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Ligada" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Premir Esc para fechar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Falha ao executar '%s'" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Interface do controlo de volume do telefone" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "As palavras-passe não coincidem." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "A palavra-passe não pode ficar em branco" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Lanterna" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Memorizar decisão" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:27 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:291 +msgid "Cancel" +msgstr "Cancelar" + +#: src/ui/app-grid-button.ui:23 +msgid "Remove from _Favorites" +msgstr "Remover dos _Favoritos" + +#: src/ui/app-grid-button.ui:28 +msgid "Add to _Favorites" +msgstr "Adicionar aos Favoritos" + +#: src/ui/app-grid-button.ui:33 +msgid "View _Details" +msgstr "_Ver detalhes" + +#: src/ui/app-grid-button.ui:41 +msgid "_Remove from Folder" +msgstr "_Remover da pasta" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Procurar aplicações…" + +#: src/ui/audio-settings.ui:85 +msgid "Output Devices" +msgstr "Dispositivos de saída" + +#: src/ui/audio-settings.ui:109 +msgid "Input Devices" +msgstr "Dispositivos de entrada" + +#: src/ui/audio-settings.ui:137 +msgid "Sound Settings" +msgstr "Definições de som" + +#: src/ui/bt-status-page.ui:31 +msgid "Enable Bluetooth" +msgstr "Ativar Bluetooth" + +#: src/ui/bt-status-page.ui:55 +msgid "Bluetooth Settings" +msgstr "Definições de Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Fechar o diálogo de chamada de emergência" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Contatos de emergência" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Vai para a página de contatos de emergência" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "Volta para a página de discador de emergência" + +#: src/ui/emergency-menu.ui:106 +msgid "Owner unknown" +msgstr "Dono desconhecido" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:232 +msgid "Emergency Contacts" +msgstr "Contatos de emergência" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Nenhum contato de emergência disponível." + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Algumas aplicações estão ocupadas ou têm trabalho não guardado" + +#: src/ui/gtk-mount-prompt.ui:88 +msgid "User:" +msgstr "Utilizador:" + +#: src/ui/gtk-mount-prompt.ui:111 +msgid "Domain:" +msgstr "Domínio:" + +#: src/ui/gtk-mount-prompt.ui:144 +msgid "Co_nnect" +msgstr "_Ligar" + +#: src/ui/lockscreen.ui:47 src/ui/lockscreen.ui:256 +msgid "Back" +msgstr "Voltar" + +#: src/ui/lockscreen.ui:98 +msgid "Slide up to unlock" +msgstr "Deslize para desbloquear" + +#: src/ui/lockscreen.ui:341 +msgid "Unlock" +msgstr "Desbloquear" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Autenticação necessária" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:91 +msgid "_Cancel" +msgstr "_Cancelar" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "_Ligar" + +#: src/ui/polkit-auth-prompt.ui:117 +msgid "Authenticate" +msgstr "Autenticar" + +#: src/ui/power-menu.ui:109 +msgid "Suspend" +msgstr "Suspender" + +#: src/ui/power-menu.ui:153 +msgid "Lock" +msgstr "Bloquear" + +#: src/ui/power-menu.ui:231 +msgid "Emergency" +msgstr "Urgente" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Executar um comando" + +#: src/ui/settings.ui:142 +msgid "No notifications" +msgstr "Sem notificações" + +#: src/ui/settings.ui:173 +msgid "Notifications" +msgstr "Notificações" + +#: src/ui/settings.ui:182 +msgid "Clear all" +msgstr "Limpar tudo" + +#: src/ui/system-prompt.ui:57 +msgid "Confirm:" +msgstr "Confirmar:" + +#: src/ui/top-panel.ui:32 +msgid "_Power Off…" +msgstr "_Desligar…" + +#: src/ui/top-panel.ui:60 +msgid "_Restart…" +msgstr "_Reiniciar…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspender…" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "_Encerrar…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:72 +msgid "Wi-Fi Settings" +msgstr "Definições de Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%A, %d de %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Extensão não encontrada" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Não foi possível carregar a extensão '%s'." + +#: src/wifi-status-page.c:51 +msgid "No Wi-Fi Device Found" +msgstr "Nenhum dispositivo Wi-Fi encontrado" + +#: src/wifi-status-page.c:55 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi desativado" + +#: src/wifi-status-page.c:56 +msgid "Enable Wi-Fi" +msgstr "Ativar Wi-Fi" + +#: src/wifi-status-page.c:59 +msgid "Wi-Fi Hotspot Active" +msgstr "Ponto de acesso Wi-Fi ativo" + +#: src/wifi-status-page.c:60 +msgid "Turn Off" +msgstr "Desligar" + +#: src/wifi-status-page.c:63 +msgid "No Wi-Fi Hotspots" +msgstr "Nenhum ponto de acesso Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Celular" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Phosh a cafeína" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Ligada" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Desligada" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Estilo predefinido" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Modo escuro" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Modo claro" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:68 +msgid "Personal Information" +msgstr "Informação pessoal" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:86 +msgid "Date of Birth" +msgstr "Data de nascimento" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:98 +msgid "Preferred Language" +msgstr "Idioma preferido" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:110 +msgid "Home Address" +msgstr "Endereço de casa" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:128 +msgid "Medical Information" +msgstr "Informação médica" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:134 +msgid "Age" +msgstr "Idade" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:146 +msgid "Blood Type" +msgstr "Tipo sanguíneo" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:158 +msgid "Height" +msgstr "Altura" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Weight" +msgstr "Peso" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:182 +msgid "Allergies" +msgstr "Alergias" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Medicamentos e Condições" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:214 +msgid "Other Information" +msgstr "Outra informação" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:17 +msgid "Emergency Info Preferences" +msgstr "Preferências de informação de emergência" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:38 +msgid "Done" +msgstr "Concluído" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:74 +msgid "Owner Name" +msgstr "Nome do proprietário" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "Medicamentos e Condições" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:241 +msgid "Add Contact" +msgstr "Adicionar contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:286 +msgid "Add New Contact" +msgstr "Adicionar novo contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:296 +msgid "Add" +msgstr "Adicionar" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:317 +msgid "New Contact Name" +msgstr "Novo nome de contacto" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:331 +msgid "Relationship" +msgstr "Relação" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:344 +msgid "Number" +msgstr "Número" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Nenhum lançador configurado" + +#: plugins/launcher-box/launcher-box.ui:32 +msgid "Launchers" +msgstr "Lançadores" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "Dados móveis ligado" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Dados móveis desligado" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Luz noturna ligada" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Luz noturna desligada" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Início do Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Concentre-se em sua tarefa por %d minutos" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Faça um intervalo" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Tem %d minutos até próximo Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Temporizador Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro desligado" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Preferências das definições rápidas do Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Técnica Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Duração _ativo" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Duração da sessão de concentração" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Duração do _intervalo" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Duração dos intervalos entre sessões" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Sem documentos para mostrar" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Bilhetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:87 +msgid "Choose Folder" +msgstr "Escolher pasta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Open" +msgstr "_Abrir" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferências da bilheteira" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Caminhos" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Folder Settings" +msgstr "Definições da pasta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:16 +msgid "Where Phosh looks for your tickets" +msgstr "Onde o Phosh procura os seus bilhetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Ticket Folder" +msgstr "Bilheteira" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hoje" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Amanhã" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "Em %u dia" +msgstr[1] "Em %u dias" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Sem eventos" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "O dia todo" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Termina" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Evento sem título" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferências dos próximos eventos" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dias" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Intervalo de datas" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Número de dias a mostrar eventos para" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "Ponto de acesso ligado" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Ponto de acesso desligado" + +#~ msgid "Unknown application" +#~ msgstr "Aplicação desconhecida" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "ms" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "a" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "a" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplicação" + +#~ msgid "Lock Screen" +#~ msgstr "Bloquear ecrã" + +#~ msgid "Logout" +#~ msgstr "Sair" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Mostrar apenas aplicações adaptativas" diff --git a/po/pt_BR.po b/po/pt_BR.po new file mode 100644 index 000000000..ecb5ae6a0 --- /dev/null +++ b/po/pt_BR.po @@ -0,0 +1,1334 @@ +# Brazilian Portuguese translaiton to phosh. +# Copyright (C) 2025 THE phosh'S COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Luís Fernando Stürmer da Rosa , 2018-2020. +# Matheus Barbosa , 2022. +# Rafael Fontenelle , 2020-2025. +# Juliano de Souza Camargo , 2024-2025. +# Álvaro Burns <>, 2025-2026. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-09 12:22+0000\n" +"PO-Revision-Date: 2026-02-09 18:07-0300\n" +"Last-Translator: Álvaro Burns <>\n" +"Language-Team: Brazilian Portuguese \n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 49.0\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Interface de telefone" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Gerenciador de janelas e lançador de aplicativos para telefone" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Esta sessão o leva ao Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Definições rápidas do Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Previne que a sessão entre em espera" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Calendário" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Um widget de calendário simples" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Modo escuro / Configurações rápidas do esquema de cores" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Alterna modo escuro" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Informação de emergência" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Mostra informação de emergência e contatos" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Caixa de lançadores" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Adiciona lançadores na tela de bloqueio. Esta extensão é experimental." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Definições rápidas da localização" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Alterna o serviço de localização" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Reprodutores de mídia" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Acompanhe os reprodutores de mídia em execução" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Configurações rápidas dos dados móveis" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Alterna os dados móveis" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Configurações rápidas da luz noturna" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Alterna luz noturna" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Configurações rápidas do Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Temporizador simples Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Bilheteria" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Mostra PDFs na tela de bloqueio. Esta extensão é experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Próximos eventos" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Mostra próximos eventos do calendário" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Adicionar à pasta" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Criar uma nova pasta" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplicativo" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Mostrar todos os aplicativos" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Mostrar apenas os aplicativos para dispositivos móveis" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Shell do controle de volume do telefone" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Bateria %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Ligado" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Sem dispositivos Bluetooth conectáveis" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth desativado" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Número desconhecido" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "A rede Wi-Fi “%s” usa um portal captivo" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "A rede Wi-Fi usa um portal captivo" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Entrar na rede Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Ancorado" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Desancorado" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Ok" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Impossível colocar chamada de emergência" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Erro interno" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Sair" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s será desconectado automaticamente em %d segundo." +msgstr[1] "%s será desconectado automaticamente em %d segundos." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Desligar" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "O sistema será desligado automaticamente em %d segundo." +msgstr[1] "O sistema será desligado automaticamente em %d segundos." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Reiniciar" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "O sistema será reiniciado automaticamente em %d segundo." +msgstr[1] "O sistema será reiniciado automaticamente em %d segundos." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Quieto" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silêncio" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Ligado" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Permitir que “%s” acesse informações da sua localização?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Localização geográfica" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Sim" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Não" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Insira a senha" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Verificando…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Captura de tela salva em '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Falha ao salvar a captura de tela" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Captura de tela" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Capturas de telas" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Captura de tela de %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Título desconhecido" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Artista desconhecido" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Tela integrada" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Desconhecido" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Tipo de autenticação da rede wifi “%s” sem suporte" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Insira a senha para a rede wifi “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Abrir" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Notificação" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "agora" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1mo" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmo" +msgstr[1] "%dmos" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Mais de %dy" +msgstr[1] "Mais de %dy" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Quase %dy" +msgstr[1] "Quase %dy" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Diálogo de autenticação dispensado pelo usuário" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Senha:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Ops! Não deu! Tente novamente." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Retrato" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Paisagem" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Desligada" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Ligada" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Pressione ESC para fechar" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Falha ao execução de “%s”" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "As senhas não conferem." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "A senha não pode ficar em branco" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Tocha" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Lembrar da decisão" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Cancelar" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Remover dos _favoritos" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Adicionar aos _favoritos" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Ver _detalhes" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Desinstalar" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Remover da pasta" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Procurar aplicativos…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Dispositivos de saída" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Dispositivos de entrada" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Configurações de som" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Brilho automático" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Configurações de brilho" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Ativar Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Configurações de Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Fechar o diálogo de chamada de emergência" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Contatos de emergência" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Vai para a página de contatos de emergência" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Volta para a página de discador de emergência" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Dono desconhecido" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Contatos de emergência" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Nenhum contato de emergência disponível." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Alguns aplicativos estão ocupados ou possuem trabalhos não salvos" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Feedback" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Não perturbe" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Configurações de feedback" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Usuário:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domínio:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "C_onectar" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Voltar" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Deslize para desbloquear" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Desbloquear" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autenticação necessária" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Cancelar" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "C_onectar" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Autenticar" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Suspender" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Bloquear" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Emergência" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Executar comando" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Sem notificações" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Notificações" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Limpar tudo" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirmar:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Desligar…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Reiniciar…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspender…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Encerrar…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Configurações de Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %d de %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Extensão não encontrada" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "A extensão '%s' não pôde ser carregada." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Nenhum dispositivo Wi-Fi encontrado" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi desativado" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Ativar Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Ponto de acesso Wi-Fi ativo" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Desligar" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Nenhum ponto de acesso Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Celular" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh em caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Desligada" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Ligada" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Temporizadores caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Sem intervalos caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Configurações do Caffeine" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Sem tempo limite (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Preferências de definições rápidas do caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Duração do caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Gerenciar duração do caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Adicione ou remova intervalos personalizados ao caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Adicionar intervalo" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Adicionar novo intervalo" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Adicionar" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Intervalos rápidos" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Escolher intervalo" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Estilo padrão" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Modo escuro" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Modo claro" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informação pessoal" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Data de nascimento" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Idioma preferido" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Endereço residencial" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informação médica" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Idade" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Tipo sanguíneo" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Altura" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Espessura" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergias" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medicação e condições" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Outras informações" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferências da informação de emergência" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Concluído" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Nome do dono" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Data de nascimento" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "Idioma _preferido" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Idade" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Tipo sanguíneo" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Altura" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Espessura" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medicação e condições" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Adicionar contato" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Adicionar novo contato" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Nome de _contato" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relação parental" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Número do _contato" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Nenhum lançador configurado" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Lançadores" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Localização ligada" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Localização desligada" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Nenhum reprodutor de mídia em execução" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Dados móveis ligado" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Dados móveis desligado" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Luz noturna ligada" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Luz noturna desligada" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Início do Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Concentre-se em sua tarefa por %d minutos" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Faça um intervalo" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Você tem %d minutos até próximo Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Temporizador Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro desligado" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Preferências das configurações rápidas do Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Técnica Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Duração _ativo" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Duração da sessão de concentração" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Duração do _intervalo " + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Duração dos intervalos entre sessões" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Inicia ao desbloquear" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Se deve iniciar o temporizador ao desbloquear a tela" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Sem documentos a exibir" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Bilhetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Abrir" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Escolher pasta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferência da caixa de bilhetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Caminhos" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Configurações de pasta" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Onde o Phosh procura por bilhetes" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Pasta de bilhetes" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Hoje" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Amanhã" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Sem eventos" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Todo o dia" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Termina" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Evento sem título" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Nenhum evento para os próximos %d dias" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Sem eventos próximos" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferências dos próximos eventos" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dias" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Intervalo de datas" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Número de dias a mostrar eventos para" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Escalas do monitor" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Ponto de acesso Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Ativar" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Ponto de acesso ligado" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Ponto de acesso desligado" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "Em %u dia" +#~ msgstr[1] "Em %u dias" + +#~ msgid "Add" +#~ msgstr "Adicionar" + +#~ msgid "Number" +#~ msgstr "Número" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Captura de tela copiada para a área de transferência" + +#~ msgid "Scan" +#~ msgstr "Varrer" + +#~ msgid "Unknown application" +#~ msgstr "Aplicativo desconhecido" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "a" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "a" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Aplicativo" + +#, fuzzy +#~| msgid "Power Off" +#~ msgid "_Power Off" +#~ msgstr "Desligar" + +#~ msgid "Lock Screen" +#~ msgstr "Tela de bloqueio" + +#~ msgid "Logout" +#~ msgstr "Encerrar sessão" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Mostrar apenas aplicativos adaptativos" + +#~ msgid "Unknown artist" +#~ msgstr "Artista desconhecido" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d.%m.%y" + +#~ msgid "Unknown Song" +#~ msgstr "Música desconhecida" diff --git a/po/ro.po b/po/ro.po new file mode 100644 index 000000000..9ac6f9a11 --- /dev/null +++ b/po/ro.po @@ -0,0 +1,1249 @@ +# Romanian translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Antonio Marin , 2024-2026. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-04 17:32+0100\n" +"Last-Translator: Antonio Marin \n" +"Language-Team: Romanian \n" +"Language: ro\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < " +"20)) ? 1 : 2);\n" +"X-Generator: Gtranslator 48.0\n" +"X-Poedit-SourceCharset: UTF-8\n" +"X-Poedit-Flags-xgettext: --add-comments\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Interfață pentru telefon" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Gestionarea ferestrelor și pornirea aplicațiilor pentru mobil" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Această sesiune permite accesul la Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Opțiuni pentru Cafeină" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Împiedică sesiunea să devină inactivă" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Calendar" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Un calendar în miniatură" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Opțiuni Aspect întunecos / Combinație de culori" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Aspect întunecos Activat/Dezactivat" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Info. de urgență" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Arată informațiile de urgență și contactele" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Cutie lansator" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Adaugă lansatoare pe ecranul blocat. Acest modul este experimental." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Opțiuni pentru Localizare" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Localizare Activată/Dezactivată" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Redare media" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Monitorizează aplicațiile active de redare audio și video" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Opțiuni pentru Date mobile" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Date mobile Activate/Dezactivate" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Opțiuni pentru Lumina de noapte" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Lumina de noapte Pornită/Oprită" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Opțiuni pentru Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Cronometru Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Casa de bilete" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Arată fișiere PDF pe ecranul blocat. Acest modul este experimental." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Evenimente viitoare" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Arată evenimentele viitoare din calendar" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Adaugă în dosar" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Creează un dosar nou" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Aplicație" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Arată toate aplicațiile" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Arată numai aplicații pentru mobil" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Controlul sunetului" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Bateria %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Pornit" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Nu s-au găsit dispozitive Bluetooth conectabile" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth dezactivat" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Apelant necunoscut" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Rețeaua Wi-Fi „%s” utilizează un portal captiv" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Rețeaua Wi-Fi utilizează un portal captiv" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Autentificare în rețeaua Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Cuplat" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Decuplat" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Bine" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Nu se poate efectua apelul de urgență" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Eroare internă" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Termină sesiunea" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s va fi deconectat automat după %d secundă." +msgstr[1] "%s va fi deconectat automat după %d secunde." +msgstr[2] "%s va fi deconectat automat după %d de secunde." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Oprește" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistemul se va opri automat după %d secundă." +msgstr[1] "Sistemul se va opri automat după %d secunde." +msgstr[2] "Sistemul se va opri automat după %d de secunde." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Repornește" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistemul va reporni automat după %d secundă." +msgstr[1] "Sistemul va reporni automat după %d secunde." +msgstr[2] "Sistemul va reporni automat după %d de secunde." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Vibrații" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Silențios" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Pornit" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "„%s” poate accesa poziția ta geografică?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Poziția geografică" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Da" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Nu" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Introdu codul de acces" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Se verifică…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Instantaneu de ecran salvat ca „%s”" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Nu s-a putut salva instantaneul de ecran" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Instantaneu de ecran" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Instantanee de ecran" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Imagine %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Titlu necunoscut" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Artist necunoscut" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Ecran integrat" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Necunoscut" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Tipul de autentificare pentru rețeaua Wi-Fi „%s” nu este acceptat" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Introdu parola pentru rețeaua Wi-Fi „%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Deschide" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Notificare" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "acum" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" +msgstr[2] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" +msgstr[2] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 zi" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d zi" +msgstr[1] "%dzile" +msgstr[2] "%dzile" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1lună" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dlună" +msgstr[1] "%dluni" +msgstr[2] "%dluni" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d an" +msgstr[1] "~%d ani" +msgstr[2] "~%d ani" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Peste %d an" +msgstr[1] "Peste %d ani" +msgstr[2] "Peste %d ani" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Aproape %d an" +msgstr[1] "Aproape %d ani" +msgstr[2] "Aproape %d ani" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Fereastra de autentificare a fost închisă de utilizator" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Parola:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Nu a funcționat. Încearcă din nou." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Vertical" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Orizontal" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Oprit" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Pornit" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Apasă ESC pentru a închide" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Executarea „%s” a eșuat" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Parolele nu se potrivesc." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Parola nu poate fi goală" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Lanternă" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Memorează decizia" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Anulează" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Elimină din Pre_ferate" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Adaugă în Pre_ferate" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Arată _detalii" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Dezinstalează" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Elimină din dosar" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Caută aplicații…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Dispozitive de ieșire" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Dispozitive de intrare" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Opțiuni Sunet" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Luminozitate automată" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Opțiuni Luminozitate" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Activează Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Opțiuni Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Închide dialogul despre apelul de urgență" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Contacte de urgență" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Du-te la pagina contactelor de urgență" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Înapoi la pagina de apelare de urgență" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Proprietar necunoscut" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Contacte de urgență" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Nu există contacte de urgență." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Unele aplicații sunt ocupate sau au lucrări nesalvate" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Reacții" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Nu deranja" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Opțiuni pentru reacții" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Utilizator:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domeniu:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "Co_nectează" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Înapoi" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Trage în sus pentru a debloca" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Deblochează" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autentificare necesară" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Anulează" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "C_onectează" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Autentificare" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Suspendă" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Blochează" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Urgență" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Execută comanda" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Nicio notificare" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Notificări" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Șterge tot" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Confirmă:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "O_prește…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Repornește…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Suspendă…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Termină sesiunea…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Opțiuni Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Modulul n-a fost găsit" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Modulul „%s” nu a putut fi încărcat." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Niciun dispozitiv Wi-Fi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi Dezactivat" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Activează Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Punct de acces Wi-Fi Activ" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Oprește" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Niciun punct de acces Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Celular" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh cu Cafeină" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Oprit" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Pornit" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Timpi Cafeină" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Niciun interval Cafeină" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Opțiuni Cafeină" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Nelimitat (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Preferințe pentru Cafeină" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Timpi Cafeină" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Gestionează durata pentru Cafeină" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Adaugă sau elimină intervale pentru Cafeină" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Adaugă un interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Adaugă un interval nou" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Adaugă" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Intervale rapide" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Alege intervalul" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Aspect prestabilit" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Aspect întunecos" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Aspect luminos" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Informații personale" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Data nașterii" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Limba preferată" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Adresa" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Informații medicale" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Vârsta" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Grupa sanguină" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Înălțimea" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Greutatea" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergii" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medicamente și Boli" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Alte informații" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Preferințe pentru Urgență" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Gata" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Numele _proprietarului" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Data nașterii" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Limba preferată" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Vârsta" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "Grupa _sanguină" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "Î_nălțimea" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Greutatea" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medicamente și Boli" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Adaugă un contact" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Adaugă un contact nou" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Numele _contactului" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relația" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Numărul _contactului" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Nu există lansatoare" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Lansatoare" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Localizare Activată" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Localizare Dezactivată" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Nicio aplicație activă de redare media" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Date mobile Activate" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Date mobile Dezactivate" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Lumina de noapte Pornită" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Lumina de noapte Oprită" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pornește Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Concentrează-te la treaba ta timp de %d minute" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Fă o pauză" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "%d minute până la următoarea activitate" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Cronometru Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Oprește Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Preferințe pentru Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Metoda Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Durata _activității" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Durata intervalului de lucru" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Durata _pauzei" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Durata pauzei dintre activități" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "Pornește la _deblocare" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Dacă să pornească cronometrul la deblocarea ecranului" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Niciun document de arătat" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Bilete" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Deschide" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Alege dosarul" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Preferințe Casa de bilete" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Căi" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Opțiuni pentru dosar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Unde vor fi căutate biletele" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Dosar Bilete" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Azi" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Mâine" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Niciun eveniment" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Toată ziua" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Se termină" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Eveniment fără titlu" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Niciun eveniment pentru următoarele %d zile" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Nu există evenimente viitoare" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Preferințe Evenimente viitoare" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Zile" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Interval" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Numărul de zile pentru care se afișează evenimentele" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Scara monitorului" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Punct de acces Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Pornește" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Punct de acces Activ" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Punct de acces Oprit" diff --git a/po/ru.po b/po/ru.po new file mode 100644 index 000000000..789c5ae41 --- /dev/null +++ b/po/ru.po @@ -0,0 +1,1336 @@ +# Jaroslav Svoboda , 2018. #zanata +# Lighting , 2019. #zanata +# Pavel , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-07 22:08+0300\n" +"Last-Translator: Artur So \n" +"Language-Team: Russian\n" +"Language: ru\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Оболочка телефона" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Управление окнами и запуск приложений для мобильных устройств" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Этот сеанс регистрирует вас в Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Быстрая настройка кофеина" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Предотвратить переход сеанса в режим ожидания" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Календарь" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Простой виджет календаря" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Темный стиль / Быстрая настройка цветовой схемы" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Переключатель темного стиля" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Экстренная помощь" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Показать экстренные сведения и контакты" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Бокс с ярлыками" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Добавить ярлыки на экран блокировки. Этот модуль является " +"экспериментальным." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Быстрая настройка геолокации" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Включение/выключение служб геолокации" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Медиапроигрыватели" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Отслеживание запущенных в данный момент медиапроигрывателей" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Быстрая настройка мобильных данных" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Включение/выключение мобильных данных" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Быстрая настройка ночного света" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Включение/выключение ночного света" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Быстрая настройка Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Простой таймер Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Билетный бокс" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Показывать PDF-файлы на экране блокировки. Этот модуль является " +"экспериментальным." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Предстоящие события" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Показать предстоящие события календаря" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Добавить в папку" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Создать новую папку" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Приложение" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Показать все приложения" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Показать приложения только для мобильных устройств" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Регулятор громкости оболочки телефона" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Батарея %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Вкл" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Подключаемых устройств Bluetooth не найдено" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth отключен" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Неизвестный абонент" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Сеть Wi-Fi '%s' использует автономный портал" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Сеть Wi-Fi использует автономный портал" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Войдите в сеть Wi-Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Прикреплено" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Откреплено" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Хорошо" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Невозможно сделать экстренный вызов" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Внутренняя ошибка" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Выйти" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s автоматически выйдет из системы через %d секунду." +msgstr[1] "%s автоматически выйдет из системы через %d секунды." +msgstr[2] "%s автоматически выйдет из системы через %d секунд." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Выключить" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Система автоматически выключится через %d секунду." +msgstr[1] "Система автоматически выключится через %d секунды." +msgstr[2] "Система автоматически выключится через %d секунд." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Перезапустить" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Система автоматически перезагрузится через %d секунду." +msgstr[1] "Система автоматически перезагрузится через %d секунды." +msgstr[2] "Система автоматически перезагрузится через %d секунд." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Тихий" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Беззвучный" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Вкл" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Разрешить '%s' доступ к информации о вашем местоположении?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Геолокация" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Да" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Нет" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Введите код доступа" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Проверка…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Снимок экрана сохранен в '%s'" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Не удалось сохранить снимок экрана" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Снимок экрана" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Снимки экрана" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Снимок экрана от %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Неизвестное название" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Неизвестный исполнитель" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Встроенный дисплей" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Неизвестный" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Тип аутентификации сети Wi-Fi “%s” не поддерживается" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Введите пароль для сети Wi-Fi “%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Открыть" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Уведомление" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "сейчас" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30 с" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 м" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 м" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d м" +msgstr[1] "%d м" +msgstr[2] "%d м" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d ч" +msgstr[1] "~%d ч" +msgstr[2] "~%d ч" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 д" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d д" +msgstr[1] "%d д" +msgstr[2] "%d д" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 мес" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d мес" +msgstr[1] "%d мес" +msgstr[2] "%d мес" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d г" +msgstr[1] "~%d г" +msgstr[2] "~%d л" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Более %d г" +msgstr[1] "Более %d л" +msgstr[2] "Более %d л" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Почти %d г" +msgstr[1] "Почти %d г" +msgstr[2] "Почти %d л" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Диалог аутентификации был отклонен пользователем" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Пароль:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Извините, это не сработало. Попробуйте снова." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Портретная" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Ландшафтная" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Выкл" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Вкл" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Нажмите Esc чтобы закрыть" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Выполнение '%s' не удалось" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Пароли не совпадают." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Пароль не может быть пустым" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Фонарик" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Запомнить решение" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Отменить" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Удалить из _Избранного" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Добавить в _Избранное" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "_Подробности" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Удалить" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Удалить из папки" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Поиск приложений…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Устройства вывода" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Устройства ввода" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Настройки звука" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Автоматическая яркость" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Настройки яркости" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Включить Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Настройки Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Закрыть диалоговое окно экстренного вызова" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Контакты для _экстренной помощи" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Перейти на страницу контактов для экстренной помощи" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Вернуться на страницу экстренного набора номера" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Владелец неизвестен" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Контакты для экстренной помощи" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Контакты для экстренной помощи отсутствуют." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Некоторые приложения заняты или имеют несохраненную работу" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Обратная связь" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Не беспокоить" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Настройки обратной связи" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Пользователь:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Домен:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "По_дключиться" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Назад" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Проведите вверх для разблокировки" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Разблокировать" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Требуется аутентификация" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Отменить" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "П_одключить" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Подтвердить" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Ожидание" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Заблокировать" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Помощь" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Выполнить команду" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Нет уведомлений" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Уведомления" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Очистить все" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Подтвердить:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Выключить…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Перезапустить…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Ожидание…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Выйти…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Настройки Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Модуль не найден" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Модуль '%s' не может быть загружен." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Устройство Wi-Fi не найдено" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi отключен" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Включить Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Точка доступа Wi-Fi активна" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Выключить" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Нет точек доступа Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Мобильная сеть" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh на кофеине" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Выкл" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Вкл" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Таймеры кофеина" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Нет интервалов кофеина" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Настройки кофеина" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Нет перерыва (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Параметры быстрой настройки кофеина" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Длительность кофеина" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Управление длительностью кофеина" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Добавить или удалить пользовательские интервалы кофеина" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Добавить интервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Добавить новый интервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Добавить" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Интервалы быстрого запуска" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 мин" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 мин" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 мин" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 час" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Выбрать интервал" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Стиль по умолчанию" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Темный стиль" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Светлый стиль" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Личная информация" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Дата рождения" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Предпочтительный язык" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Домашний адрес" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Медицинская информация" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Возраст" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Группа крови" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Рост" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Вес" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Аллергии" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Лекарства и условия" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Прочая информация" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Параметры инфо экстренной помощи" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Готово" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Имя в_ладельца" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Дата рождения" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Предпочтительный язык" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "В_озраст" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "Группа _крови" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Рост" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Вес" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Лекарства и условия" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Добавить контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Добавить новый контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Имя _контакта" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Родство" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Контактный _номер" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Ярлыки не настроены" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Ярлыки" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Геолокация вкл" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Геолокация выкл" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Нет запущенных медиапроигрывателей" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Моб. данные включены" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Моб. данные выключены" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Ночной свет включен" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Ночной свет выключен" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Запустить Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Сфокусироваться на задаче на %d мин" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Сделать перерыв" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "У вас осталось %d мин. до следующего Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Таймер Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro выключен" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Параметры быстрой настройки Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Методика Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Длительность _активности" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Длительность сеанса фокусировки" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Длительность _перерыва" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Длительность перерыва между сеансами" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Запустить при разблокировке" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Следует ли запускать таймер при разблокировке экрана" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Нет документов для отображения" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Билеты" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "О_ткрыть" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Выбрать папку" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Параметры билетного бокса" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Пути" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Настройки папки" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Где Phosh ищет ваши билеты" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Папка с билетами" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Сегодня" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Завтра" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x, %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Нет событий" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l∶%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Весь день" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Конец" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Безымянное событие" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Нет событий в течение следующих %d дней" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Нет предстоящих событий" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Параметры предстоящих событий" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Дни" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Интервал дат" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Количество дней для показа событий" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Масштаб экрана" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Точка доступа Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Включить" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Точка доступа включена" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Точка доступа выключена" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "Через %u день" +#~ msgstr[1] "Через %u дня" +#~ msgstr[2] "Через %u дней" + +#~ msgid "Add" +#~ msgstr "Добавить" + +#~ msgid "Number" +#~ msgstr "Номер" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Снимок экрана скопирован в буфер обмена" + +#~ msgid "Scan" +#~ msgstr "Сканировать" + +#~ msgid "_Power Off" +#~ msgstr "_Выкл." + +#~ msgid "_Screenshot" +#~ msgstr "_Снимок" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "с" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "м" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "м" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "ч" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "ч" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "мес" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "мес" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "г" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "г(л)" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Приложение" + +#~ msgid "Unknown application" +#~ msgstr "Неизвестное приложение" + +#~ msgid "Lock Screen" +#~ msgstr "Заблокировать экран" + +#~ msgid "Logout" +#~ msgstr "Выйти" + +#, c-format +#~ msgid "On %A" +#~ msgstr "В(о) %A" diff --git a/po/sat.po b/po/sat.po new file mode 100644 index 000000000..1717358f3 --- /dev/null +++ b/po/sat.po @@ -0,0 +1,1319 @@ +# Santali translation for phosh. +# Copyright (C) 2022 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Prasanta Hembram , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-12-24 16:01+0000\n" +"PO-Revision-Date: 2025-12-30 19:39+0530\n" +"Last-Translator: Prasanta Hembram \n" +"Language-Team: Santali \n" +"Language: sat\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "ᱯᱷᱚᱱ ᱪᱚᱠᱟᱜ" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "ᱢᱳᱵᱟᱭᱤᱞ ᱞᱟᱹᱜᱤᱫ ᱣᱤᱱᱰᱚ ᱢᱮᱱᱮᱡᱽᱢᱮᱸᱱᱴ ᱟᱨ ᱮᱯᱞᱤᱠᱮᱥᱚᱱ" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "ᱱᱚᱶᱟ ᱠᱟᱹᱢᱤᱦᱚᱨᱟ ᱫᱚ Phosh ᱛᱮ ᱤᱫᱤ ᱢᱮᱭᱟᱭ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Caffeine ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "ᱥᱮᱥᱚᱱ ᱫᱚ ᱵᱟᱝ ᱠᱟᱹᱢᱤ ᱛᱟᱦᱮᱱ ᱠᱷᱚᱱ ᱵᱚᱱᱫᱚᱭ ᱢᱮ" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "ᱢᱟᱦᱮᱛᱟ" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "ᱢᱤᱫ ᱥᱟᱫᱷᱟᱨᱚᱬ ᱢᱟᱦᱮᱛᱟ ᱣᱤᱡᱮᱴ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "ᱧᱩᱛ ᱢᱚᱰ/ᱨᱚᱝ ᱯᱚᱱᱛᱷᱟ ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "ᱧᱩᱛ ᱢᱚᱰ ᱴᱚᱜᱽᱞ ᱢᱮ" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "ᱤᱢᱟᱨᱡᱮᱱᱥᱤ ᱵᱟᱯᱟᱰᱟᱭ" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "ᱤᱢᱟᱨᱡᱮᱱᱥᱤ ᱵᱮᱣᱨᱟ ᱟᱨ ᱡᱚᱯᱲᱟᱣ ᱠᱚ ᱩᱫᱩᱜ ᱢᱮ" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "ᱞᱚᱱᱪᱟᱨ ᱵᱟᱠᱚᱥ" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"ᱞᱚᱠ ᱥᱠᱨᱤᱱ ᱨᱮ ᱞᱚᱱᱪᱟᱨ ᱥᱮᱞᱮᱫ ᱢᱮ᱾ ᱱᱚᱣᱟ ᱯᱞᱟᱜᱤᱱ ᱫᱚ ᱵᱮᱵᱷᱟᱨ ᱞᱮᱠᱟᱱᱟᱜ ᱠᱟᱱᱟ ᱾" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "ᱴᱷᱟᱶ ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "ᱴᱚᱜᱽᱞ ᱴᱷᱟᱶ ᱥᱮᱵᱟ ᱪᱟᱹᱞᱩ/ᱵᱚᱱᱫᱚ" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "ᱢᱤᱰᱤᱭᱟ ᱠᱷᱮᱞᱚᱱᱰ" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "ᱱᱤᱛᱚᱜ ᱪᱟᱹᱞᱩ ᱠᱟᱱ ᱢᱤᱰᱤᱭᱟ ᱯᱞᱮᱭᱟᱨ ᱠᱚ ᱴᱨᱮᱠ ᱢᱮ" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "ᱢᱚᱵᱟᱭᱤᱞ ᱰᱟᱴᱟ ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "ᱢᱚᱵᱟᱭᱤᱞ ᱰᱟᱴᱟ ᱪᱟᱹᱞᱩ/ᱵᱚᱱᱫᱚ ᱴᱚᱜᱽᱞ ᱢᱮ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "ᱧᱤᱫᱟᱹ ᱢᱟᱨᱥᱟᱞ ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "ᱧᱤᱱᱫᱟᱹ ᱢᱟᱨᱥᱟᱞ ᱪᱟᱹᱞᱩ/ᱵᱚᱸᱫ ᱴᱚᱜᱽᱞ ᱢᱮ" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "ᱯᱚᱢᱚᱰᱚᱨᱳ ᱩᱥᱟᱹᱨᱟ ᱥᱮᱴᱤᱝ" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "ᱟᱞᱜᱟ ᱯᱚᱢᱚᱰᱚᱨᱚ ᱴᱟᱭᱢᱟᱨ" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "ᱴᱤᱠᱮᱴ ᱵᱟᱜᱥᱟ" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"ᱠᱩᱞᱩᱯ ᱥᱠᱨᱤᱱ ᱨᱮ PDF ᱠᱚ ᱩᱫᱩᱜ ᱢᱮ ᱾ ᱱᱚᱶᱟ ᱯᱞᱚᱜᱤᱱ ᱫᱚ ᱮᱠᱥᱯᱮᱨᱤᱢᱮᱱᱴᱟᱞ ᱜᱮᱭᱟ ᱾" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "ᱦᱟᱹᱡᱩᱜ ᱠᱟᱱ ᱜᱚᱴᱱᱟᱠᱚ" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "ᱦᱟᱹᱡᱩᱜ ᱠᱟᱱ ᱠᱮᱞᱮᱱᱰᱟᱨ ᱜᱚᱴᱱᱟᱠᱚ ᱩᱫᱩᱜᱽ ᱢᱮ" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "ᱠᱩᱥᱤᱭᱟᱜ ᱠᱚᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱢᱮ" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "ᱱᱟᱶᱟ ᱯᱷᱚᱞᱰᱟᱨ ᱵᱮᱱᱟᱣ ᱢᱮ" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "ᱮᱯᱞᱤᱠᱮᱥᱚᱱ" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "ᱡᱷᱚᱛᱚ ᱮᱯ ᱫᱮᱠᱷᱟᱣ ᱢᱮ" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "ᱠᱷᱟᱹᱞᱤ ᱢᱳᱵᱟᱭᱤᱞ ᱡᱚᱲᱟᱣ ᱮᱯ ᱠᱚ ᱫᱮᱠᱷᱟᱣ ᱢᱮ" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "ᱵᱮᱴᱟᱨᱤ %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "ᱵᱞᱩᱴᱩᱛᱷ" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "ᱮᱢ ᱪᱷᱚ" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "ᱡᱚᱯᱲᱟᱣ ᱞᱮᱠᱟᱱ ᱵᱞᱩᱴᱩᱛᱷ ᱥᱟᱫᱷᱚᱱ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "ᱵᱞᱩᱴᱩᱛᱷ" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭᱟᱜ ᱧᱩᱛᱩᱢ" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱱᱮᱴᱣᱟᱨᱠ '%s' ᱢᱤᱫ ᱠᱮᱯᱴᱤᱵᱷ ᱯᱚᱨᱴᱟᱞ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱱᱮᱴᱣᱟᱨᱠ ᱫᱚ ᱠᱮᱯᱴᱤᱵᱷ ᱯᱚᱨᱴᱟᱞ ᱵᱮᱵᱷᱟᱨ ᱮᱫᱟᱭ" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱱᱮᱴᱣᱟᱨᱠ ᱨᱮ ᱥᱟᱭᱤᱱ ᱢᱮ" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "ᱰᱚᱠ ᱮᱱᱟ" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "ᱰᱚᱠ ᱚᱪᱚᱜᱽ ᱮᱱᱟ" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "ᱴᱷᱤᱠ" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱠᱚᱞ ᱠᱚᱨᱟᱣ ᱵᱟᱭ ᱜᱟᱱ ᱞᱮᱱᱟ" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "ᱵᱷᱤᱛᱨᱤ ᱵᱷᱩᱞ" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "ᱵᱟᱦᱨᱮ ᱛᱮ ᱚᱰᱚᱠ" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s ᱫᱚ %d ᱨᱤᱡ ᱨᱮ ᱵᱟᱦᱨᱮ ᱚᱰᱚᱠᱚᱜᱼᱟᱢ ᱾" +msgstr[1] "%s ᱫᱚ %d ᱨᱤᱡ ᱨᱮ ᱵᱟᱦᱨᱮ ᱚᱰᱚᱠᱚᱜᱼᱟᱢ ᱾" +msgstr[2] "%s ᱫᱚ %d ᱨᱤᱡ ᱨᱮ ᱵᱟᱦᱨᱮ ᱚᱰᱚᱠᱚᱜᱼᱟᱢ ᱾" + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "ᱯᱟᱣᱟᱨ ᱵᱚᱸᱫᱽ" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱵᱚᱸᱫᱚᱜᱼᱟ ᱾" +msgstr[1] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱵᱚᱸᱫᱚᱜᱼᱟ ᱾" +msgstr[2] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱵᱚᱸᱫᱚᱜᱼᱟ ᱾" + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "ᱫᱚᱦᱲᱟ ᱮᱦᱚᱵ" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱮᱛᱦᱚᱵᱚᱜᱼᱟ ᱾" +msgstr[1] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱮᱛᱦᱚᱵᱚᱜᱼᱟ ᱾" +msgstr[2] "ᱥᱭᱥᱴᱚᱢ ᱫᱚ %d ᱴᱤᱡ ᱨᱮ ᱟᱡ ᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱮᱛᱦᱚᱵᱚᱜᱼᱟ ᱾" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "ᱥᱟᱸᱛ" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "ᱪᱩᱯ" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "ᱮᱢ ᱪᱷᱚ" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "ᱟᱢᱟᱜ ᱡᱟᱭᱜᱟ ᱟᱫᱮᱨ ᱪᱷᱚ ᱞᱟᱹᱜᱤᱫ '%s' ᱟᱫᱮᱨ ᱪᱷᱚᱭᱮᱢ?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "ᱡᱤᱭᱚ ᱡᱟᱭᱜᱟ" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "ᱦᱚᱭ" + +#: src/location-manager.c:272 +msgid "No" +msgstr "ᱵᱟᱝ" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "ᱯᱟᱥᱠᱳᱰ ᱟᱫᱮᱨ ᱢᱮ" + +#: src/lockscreen.c:1040 +msgid "Checking…" +msgstr "ᱧᱮᱞ ᱵᱤᱰᱟᱹᱣᱜ ᱠᱟᱱᱟ ..." + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "ᱥᱠᱨᱤᱱ ᱥᱚᱴ '%s' ᱨᱮ ᱥᱟᱸᱪᱟᱣ ᱟᱠᱟᱱᱟ ᱾" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "ᱥᱠᱨᱤᱱ ᱥᱚᱴ ᱥᱟᱧᱪᱟᱣ ᱨᱮ ᱵᱟᱝ ᱥᱟᱹᱛ ᱟᱠᱟᱱᱟ" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "ᱥᱠᱨᱤᱱᱥᱚᱴ" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "ᱥᱠᱨᱤᱱᱥᱚᱴ" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "%s%s.png ᱠᱷᱚᱱ ᱥᱠᱨᱤᱱ ᱥᱚᱴ" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:690 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭᱟᱜ ᱧᱩᱛᱩᱢ" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:698 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭᱟᱜ ᱟᱴᱤᱥᱴ" + +#: src/monitor-manager.c:128 +msgid "Built-in display" +msgstr "ᱢᱟᱲᱟᱝ ᱛᱮᱭᱟᱜ ᱫᱮᱠᱷᱟᱣ" + +#: src/monitor-manager.c:146 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:153 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:162 +msgid "Unknown" +msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭᱟᱜ" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "ᱟᱫᱮᱨ ᱞᱮᱠᱷᱟ ᱣᱟᱭᱯᱷᱟᱭ ᱱᱮᱴᱣᱟᱨᱠ “%s” ᱫᱚ ᱵᱟᱝ ᱥᱟᱹᱯᱚᱴ ᱠᱟᱱᱟ" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "ᱣᱟᱭᱯᱷᱟᱭ ᱱᱮᱴᱣᱟᱨᱠ “%s” ᱞᱟᱹᱜᱤᱫ ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫ ᱟᱫᱮᱨ ᱢᱮ" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "ᱠᱷᱩᱞᱟᱹ" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "ᱠᱷᱚᱵᱚᱨ" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "ᱱᱤᱛᱚᱜ" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~᱑m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" +msgstr[2] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" +msgstr[2] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~᱑d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" +msgstr[2] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~᱑ᱢᱚ" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmo" +msgstr[1] "%dmo" +msgstr[2] "%dmo" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" +msgstr[2] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%dᱥᱮᱨᱢᱟ ᱫᱷᱟᱹᱵᱤᱡ" +msgstr[1] "%dᱥᱮᱨᱢᱟ ᱫᱷᱟᱹᱵᱤᱡ" +msgstr[2] "%dᱥᱮᱨᱢᱟ ᱫᱷᱟᱹᱵᱤᱡ" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "%dᱥᱮᱨᱢᱟ ᱜᱟᱱ" +msgstr[1] "%dᱥᱮᱨᱢᱟ ᱜᱟᱱ" +msgstr[2] "%dᱥᱮᱨᱢᱟ ᱜᱟᱱ" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ ᱫᱚ ᱟᱫᱮᱨ ᱠᱷᱚᱵᱚᱨ ᱵᱚᱸᱫ ᱠᱮᱜᱼᱟᱭ" + +#: src/polkit-auth-prompt.c:275 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "ᱫᱟᱱᱟᱲ ᱥᱟᱵᱟᱫ :" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "ᱤᱠᱟᱹ , ᱚᱱᱟ ᱫᱚ ᱵᱟᱭ ᱠᱟᱹᱢᱤ ᱞᱮᱫᱟᱭ ᱾ ᱫᱟᱭᱟᱠᱟᱛᱮ ᱫᱩᱦᱲᱟᱹ ᱠᱩᱨᱩᱢᱩᱴᱩᱭ ᱢᱮ ᱾" + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "ᱥᱤᱫᱷ" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "ᱩᱞᱴᱟᱹ ᱞᱮᱱᱰᱥᱠᱮᱯ" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "ᱵᱚᱸᱫᱽ" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "ᱮᱢ ᱪᱷᱚ" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "ᱵᱚᱸᱫ ᱞᱟᱹᱜᱤᱫ ESC ᱚᱛᱟᱭ ᱢᱮ" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "'%s' ᱫᱟᱹᱲ ᱪᱷᱚ ᱨᱮ ᱰᱤᱜᱟᱹᱣ ᱮᱱᱟ" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "ᱯᱷᱚᱱ ᱥᱮᱞ ᱣᱚᱞᱭᱩᱢ ᱠᱚᱱᱴᱨᱚᱞ" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫ ᱫᱚ ᱵᱟᱭ ᱢᱮᱲᱟᱣ ᱞᱮᱱᱟ ᱾" + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "ᱫᱟᱱᱟᱝ ᱥᱟᱵᱟᱫ ᱫᱚ ᱠᱷᱟᱹᱞᱤ ᱵᱟᱭ ᱛᱟᱦᱮᱸᱱᱟ" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "ᱴᱚᱨᱪ" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "ᱱᱚᱶᱟ ᱴᱷᱟᱹᱣᱠᱟᱹ ᱢᱚᱱᱮ ᱫᱚᱦᱚ ᱠᱟᱜ ᱢᱮ" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "ᱵᱟᱹᱰᱨᱟᱹ" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "ᱠᱩᱥᱤᱭᱟᱜ ᱠᱷᱚᱱ ᱚᱪᱮᱜᱽ ᱢᱮ" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "ᱠᱩᱥᱤᱭᱟᱜ ᱠᱚᱨᱮ ᱥᱮᱞᱮᱫᱽ ᱢᱮ" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "ᱵᱤᱵᱨᱚᱬ ᱠᱚ ᱧᱮᱞ ᱢᱮ" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "ᱟᱱᱤᱱᱥᱴᱚᱞ ᱢᱮ" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "ᱠᱩᱥᱤᱭᱟᱜ ᱠᱷᱚᱱ ᱚᱪᱮᱜᱽ ᱢᱮ" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "ᱮᱯᱠᱚ ᱥᱮᱸᱫᱽᱨᱟᱭ ᱢᱮ ..." + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "ᱟᱩᱴᱯᱩᱴ ᱰᱤᱵᱷᱟᱭᱤᱥ" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "ᱤᱱᱯᱩᱴ ᱥᱟᱫᱷᱚᱱ ᱠᱚ" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "ᱯᱚᱴᱚᱢ ᱥᱟᱡᱟᱣ ᱠᱚ" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "ᱟᱡ ᱛᱮ ᱪᱟᱞᱟᱜ ᱡᱷᱟᱞᱠᱟᱣ" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "ᱵᱨᱟᱭᱤᱴᱱᱮᱥ ᱥᱮᱴᱤᱝ" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "ᱵᱞᱩᱴᱩᱛᱷ" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "ᱵᱞᱩᱴᱩᱛᱷ" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱠᱚᱞ ᱰᱟᱭᱞᱚᱜᱽ ᱵᱚᱸᱫᱚᱭ ᱢᱮ" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱥᱚᱢᱯᱚᱨᱠ ᱠᱚ" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "ᱮᱢᱟᱨᱡᱮᱱᱥᱤ ᱠᱚᱱᱴᱟᱠᱴ ᱥᱟᱦᱴᱟ ᱨᱮ ᱥᱮᱱᱚᱜ ᱢᱮ" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱰᱟᱭᱞᱯᱮᱰ ᱥᱟᱦᱴᱟ ᱛᱮ ᱥᱮᱱᱚᱜ ᱢᱮ" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭᱤᱡ ᱢᱟᱞᱤᱠ" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱥᱚᱢᱯᱚᱨᱠ ᱠᱚ" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱥᱚᱢᱯᱚᱨᱠ ᱠᱚ ᱵᱟᱹᱱᱩᱜᱼᱟ ᱾" + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "ᱛᱤᱱᱟᱹᱜ ᱜᱟᱱ ᱮᱯᱞᱤᱠᱮᱥᱚᱱ ᱠᱚᱫᱚ ᱵᱡᱤ ᱜᱮᱭᱟ ᱵᱟᱝᱠᱷᱟᱱ ᱠᱟᱹᱢᱤ ᱵᱟᱝ ᱥᱟᱺᱧᱟᱣ ᱠᱟᱱᱟ" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "ᱢᱚᱱᱚᱛ ᱨᱩᱣᱟᱹᱲ" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "ᱟᱞᱚᱢ ᱰᱤᱥᱴᱟᱨᱵ ᱫᱟᱲᱮᱭᱟᱜᱼᱟ" + +#: src/ui/feedback-status-page.ui:52 +msgid "Feedback Settings" +msgstr "ᱯᱚᱴᱚᱢ ᱥᱟᱡᱟᱣ ᱠᱚ" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "ᱵᱮᱵᱷᱟᱨᱤᱭᱟᱹ :" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "ᱰᱳᱢᱮᱱ :" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "ᱡᱩᱲᱟᱹᱣ" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "ᱛᱟᱭᱟᱢ" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "ᱠᱷᱩᱞᱟᱹ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱛᱟᱱ ᱥᱮᱫ ᱴᱷᱮᱞᱟᱣ ᱢᱮ" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "ᱠᱷᱩᱞᱟᱹ" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "ᱟᱫᱮᱨ ᱫᱚᱨᱠᱟᱨ ᱠᱟᱱᱟ" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "ᱵᱟᱹᱰᱨᱟᱹ" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "ᱡᱩᱲᱟᱹᱣ" + +#: src/ui/polkit-auth-prompt.ui:97 +msgid "Authenticate" +msgstr "ᱟᱫᱮᱨ" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "ᱵᱚᱸᱫᱚᱭ ᱢᱮ" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "ᱠᱩᱞᱩᱯ" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "ᱟᱹᱯᱟᱹᱛᱠᱟᱹᱞ" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "ᱟᱫᱮᱥ ᱫᱟᱹᱲ ᱪᱷᱚ" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "ᱠᱷᱚᱵᱚᱨ" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "ᱠᱷᱚᱵᱚᱨ" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "ᱡᱷᱚᱛᱚ ᱯᱷᱟᱨᱪᱟᱭ ᱢᱮ" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "ᱴᱷᱟᱹᱣᱠᱟᱹ :" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "ᱯᱟᱣᱟᱨ ᱵᱚᱸᱫᱽ" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "ᱫᱚᱦᱲᱟ ᱮᱦᱚᱵ" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_ᱵᱚᱸᱫᱽ..." + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "ᱵᱟᱦᱨᱮ ᱛᱮ ᱚᱰᱚᱠ" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "ᱯᱚᱴᱚᱢ ᱥᱟᱡᱟᱣ ᱠᱚ" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "ᱯᱞᱩᱡᱤᱱ ᱵᱟᱝ ᱧᱟᱢ ᱞᱮᱱᱟ" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "ᱯᱞᱚᱜᱤᱱ '%s' ᱫᱚ ᱞᱟᱫᱮ ᱵᱟᱭ ᱜᱟᱱ ᱞᱮᱱᱟ ᱾" + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "ᱪᱮᱫ ᱣᱟᱭ-ᱯᱷᱟᱭ ᱥᱟᱫᱷᱚᱱ ᱵᱟᱝ ᱧᱟᱢ ᱟᱠᱟᱱᱟ" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱚᱠᱟᱹᱡᱽᱣᱟᱹ ᱜᱮᱭᱟ" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱪᱟᱹᱞᱩᱭ ᱢᱮ" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱦᱚᱴᱥᱯᱚᱴ ᱮᱠᱴᱤᱵᱷ" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "ᱵᱚᱱᱫᱚ ᱢᱮ" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱦᱚᱴᱥᱯᱚᱴ ᱵᱟᱹᱱᱩᱜᱼᱟ" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "ᱯᱷᱚᱱ ᱠᱚᱛᱮ" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "ᱠᱮᱯᱷᱤᱱ ᱨᱮ ᱯᱷᱚᱥ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "ᱵᱚᱸᱫᱽ" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "ᱮᱢ ᱪᱷᱚ" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "ᱠᱮᱯᱷᱤᱱ ᱴᱟᱭᱢᱟᱨ" + +#: plugins/caffeine-quick-setting/qs.ui:37 +msgid "No caffeine intervals" +msgstr "ᱠᱮᱯᱷᱤᱱ ᱛᱟᱞᱟ ᱵᱟᱹᱱᱩᱜᱼᱟ" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "ᱚᱠᱛᱚ ᱪᱟᱵᱟ ᱵᱟᱹᱱᱩᱜᱼᱟ (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "ᱴᱤᱠᱮᱴ ᱵᱚᱠᱥ ᱠᱩᱥᱤᱭᱟᱜ ᱠᱚ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "ᱠᱮᱯᱷᱤᱱ ᱚᱠᱛᱚ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "ᱠᱮᱯᱷᱤᱱ ᱚᱠᱛᱚ ᱪᱟᱞᱟᱣ ᱢᱮ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "ᱠᱟᱥᱴᱚᱢ ᱠᱮᱯᱷᱤᱱ ᱛᱟᱞᱟ ᱛᱟᱞᱟ ᱨᱮ ᱥᱮᱞᱮᱫ ᱟᱨ ᱚᱪᱚᱜ ᱢᱮ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "ᱵᱷᱤᱛᱨᱤ ᱥᱮᱞᱮᱫ ᱢᱮ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "ᱱᱟᱶᱟ ᱵᱷᱤᱛᱨᱤ ᱥᱮᱞᱮᱫ ᱢᱮ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_ᱥᱮᱞᱮᱫ ᱢᱮ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "ᱩᱥᱟᱹᱨᱟ ᱮᱛᱚᱦᱚᱵ ᱛᱟᱞᱟ ᱛᱟᱞᱟ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "᱕ ᱢᱤ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "᱑᱕ ᱢᱤ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "᱓᱐ ᱢᱤ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "᱑ ᱴᱟᱲᱟᱝ" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "ᱵᱷᱤᱛᱨᱤ ᱵᱟᱪᱷᱟᱣ ᱢᱮ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "ᱰᱤᱯᱷᱚᱞᱴ ᱥᱴᱟᱭᱤᱞ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "ᱧᱩᱛ ᱢᱚᱰ" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "ᱞᱟᱭᱤᱴ ᱢᱚᱰ" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "ᱱᱤᱡᱚᱨ ᱵᱚᱵᱚᱛ" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "ᱡᱟᱱᱟᱢ ᱢᱟᱦᱟᱸ" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "ᱠᱩᱥᱤ ᱯᱟᱹᱨᱥᱤᱠᱚ" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "ᱚᱲᱟᱜ ᱴᱷᱤᱠᱬᱟᱹ" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "ᱢᱮᱰᱤᱠᱟᱞ ᱤᱱᱯᱷᱚᱨᱢᱮᱥᱚᱱ" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "ᱩᱢᱚᱨ" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "ᱢᱟᱭᱟᱢ ᱯᱨᱚᱠᱟᱨ" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "ᱩᱥᱩᱞ " + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "ᱦᱟᱢᱟᱞ" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "ᱟᱞᱮᱨᱡᱤ" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "ᱢᱮᱰᱤᱠᱟᱞ ᱟᱨ ᱠᱚᱱᱰᱤᱥᱚᱱ" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "ᱮᱴᱟᱜ ᱡᱚᱱᱤᱥ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "ᱮᱢᱚᱨᱡᱮᱱᱥᱤ ᱵᱤᱵᱨᱚᱬ ᱠᱩᱥᱤᱠᱚ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "ᱦᱩᱭᱮᱱᱟ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "ᱢᱟᱞᱤᱠ ᱧᱩᱱᱩᱢ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "ᱡᱟᱱᱟᱢ ᱢᱟᱦᱟᱸ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "ᱠᱩᱥᱤ ᱯᱟᱹᱨᱥᱤᱠᱚ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_ᱩᱢᱮᱨ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "ᱢᱟᱭᱟᱢ ᱯᱨᱚᱠᱟᱨ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "ᱩᱥᱩᱞ " + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "ᱦᱟᱢᱟᱞ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "ᱵᱮᱰᱟᱠ ᱟᱨ ᱵᱮᱰᱟᱠ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "ᱥᱟᱸᱜᱷᱟᱨ ᱥᱮᱞᱮᱫᱽ ᱢᱮ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "ᱱᱟᱶᱟ ᱥᱚᱢᱯᱚᱨᱠ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "ᱱᱟᱶᱟ ᱥᱚᱢᱯᱚᱨᱠ ᱧᱩᱛᱩᱢ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "ᱥᱟᱹᱜᱟᱹᱭ" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "ᱱᱟᱶᱟ ᱥᱚᱢᱯᱚᱨᱠ ᱧᱩᱛᱩᱢ" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "ᱞᱚᱱᱪᱟᱨ ᱠᱚ ᱵᱟᱝ ᱠᱚᱱᱯᱷᱤᱜᱟᱨ ᱟᱠᱟᱱᱟ" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "ᱞᱚᱱᱪᱟᱨ" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "ᱡᱤᱭᱚ ᱡᱟᱭᱜᱟ" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "ᱡᱤᱭᱚ ᱡᱟᱭᱜᱟ" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "ᱢᱤᱰᱤᱭᱟ ᱯᱞᱮᱭᱟᱨ ᱵᱟᱝ ᱪᱟᱹᱞᱩᱜ ᱠᱟᱱᱟ" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "ᱢᱚᱵᱟᱭᱤᱞ ᱰᱟᱴᱟ ᱪᱟᱹᱞᱩ" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "ᱢᱚᱵᱟᱭᱤᱞ ᱰᱟᱴᱟ ᱵᱚᱱᱫᱚ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "ᱧᱤᱫᱟᱹ ᱢᱟᱨᱥᱟᱞ ᱪᱟᱹᱞᱩ" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "ᱧᱤᱫᱟᱹ ᱢᱟᱨᱥᱟᱞ ᱵᱚᱱᱫᱚ" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "ᱯᱚᱢᱚᱰᱚᱨᱳ ᱮᱦᱚᱵᱚᱜ ᱠᱟᱱᱟ" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "%d ᱴᱤᱯᱤᱡ ᱞᱟᱹᱜᱤᱫ ᱟᱢᱟᱜ ᱠᱟᱹᱢᱤ ᱨᱮ ᱯᱷᱚᱠᱟᱥ ᱢᱮ" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "ᱢᱤᱫ ᱡᱤᱨᱟᱹᱣ ᱦᱟᱛᱟᱣ ᱢᱮ" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "ᱫᱚᱥᱟᱨ ᱯᱚᱢᱚᱰᱚᱨᱚ ᱦᱟᱹᱵᱤᱡ ᱟᱢ ᱴᱷᱮᱱ %d ᱴᱤᱯᱤᱡ ᱢᱮᱱᱟᱜᱼᱟ" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "ᱯᱚᱢᱚᱰᱚᱨᱚ ᱴᱟᱭᱢᱟᱨ" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "ᱯᱟᱣᱟᱨ ᱵᱚᱸᱫᱽ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "ᱴᱤᱠᱮᱴ ᱵᱚᱠᱥ ᱠᱩᱥᱤᱭᱟᱜ ᱠᱚ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "ᱯᱚᱢᱚᱰᱚᱨᱚ ᱴᱮᱠᱱᱤᱠ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "ᱮᱠᱴᱤᱵᱷ ᱚᱠᱛᱚ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "ᱯᱷᱚᱠᱟᱥ ᱥᱮᱥᱚᱱ ᱨᱮᱭᱟᱜ ᱚᱠᱛᱚ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "ᱡᱤᱨᱟᱹᱣ ᱚᱠᱛᱚ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "ᱫᱷᱚᱱ ᱛᱟᱞᱟ ᱨᱮ ᱡᱤᱨᱟᱹᱣ ᱨᱮᱭᱟᱜ ᱚᱠᱛᱚ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_ᱩᱱᱞᱚᱠ ᱨᱮ ᱮᱦᱚᱵ ᱢᱮ" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "ᱥᱠᱨᱤᱱ ᱚᱱᱞᱚᱠ ᱨᱮ ᱴᱟᱭᱢᱟᱨ ᱮᱦᱚᱵ ᱦᱩᱭᱩᱜᱼᱟ ᱥᱮ ᱵᱟᱝᱟ" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "ᱫᱮᱠᱷᱟᱣ ᱞᱟᱹᱜᱤᱫ ᱫᱚᱞᱤᱞ ᱵᱟᱹᱱᱩᱜᱼᱟ" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "ᱴᱤᱠᱮᱴ ᱠᱚ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "ᱠᱷᱩᱞᱟᱹ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "ᱯᱚᱴᱚᱢ ᱵᱟᱪᱷᱟᱣ ᱢᱮ …" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "ᱴᱤᱠᱮᱴ ᱵᱚᱠᱥ ᱠᱩᱥᱤᱭᱟᱜ ᱠᱚ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "ᱦᱚᱨ ᱠᱚ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "ᱯᱚᱴᱚᱢ ᱥᱟᱡᱟᱣ ᱠᱚ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "ᱡᱟᱦᱟᱸᱨᱮ Phosh ᱟᱢᱟᱜ ᱴᱤᱠᱮᱴ ᱮ ᱯᱟᱱᱛᱮ ᱵᱟᱲᱟᱭᱟᱭ" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "ᱴᱤᱠᱮᱴ ᱯᱚᱴᱚᱢ" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "ᱛᱮᱦᱮᱸᱧ" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "ᱜᱟᱯᱟ" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "ᱡᱟᱦᱟᱱ ᱠᱷᱚᱵᱚᱨ ᱠᱚ ᱵᱟ.ᱱᱩᱜᱼᱟ" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "ᱡᱷᱚᱛᱚ ᱫᱤᱱ" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "ᱢᱩᱪᱟᱹᱫ" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "ᱵᱟᱝᱧᱩᱛᱩᱢ ᱜᱚᱴᱱᱟ" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "ᱫᱚᱥᱟᱨ %d ᱫᱤᱱ ᱞᱟᱹᱜᱤᱫ ᱪᱮᱫ ᱜᱷᱚᱴᱚᱱ ᱵᱟᱹᱱᱩᱜᱼᱟ" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "ᱦᱟᱹᱡᱩᱜ ᱠᱟᱱ ᱜᱚᱴᱱᱟᱠᱚ" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "ᱦᱟᱹᱡᱩᱜ ᱠᱟᱱ ᱜᱚᱴᱱᱟᱠᱚ" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "ᱫᱤᱱ" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "ᱢᱟᱹᱦᱤᱛ ᱯᱟᱥᱱᱟᱣ" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "ᱜᱷᱚᱴᱚᱱ ᱩᱫᱩᱜ ᱞᱟᱹᱜᱤᱫ ᱫᱤᱱ ᱨᱮᱭᱟᱜ ᱮᱞ" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "ᱢᱚᱱᱤᱴᱚᱨ ᱥᱠᱮᱞ" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "ᱣᱟᱭ-ᱯᱷᱟᱭ ᱦᱚᱴᱥᱯᱚᱴ" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "ᱪᱟᱹᱞᱩᱭ ᱢᱮ" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "ᱦᱚᱴᱥᱯᱚᱴ ᱪᱟᱹᱞᱩ" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "ᱦᱚᱴᱥᱯᱚᱴ ᱵᱚᱱᱫᱚ" + +#~ msgid "Unknown application" +#~ msgstr "ᱵᱟᱝ ᱵᱟᱰᱟᱭ ᱮᱯᱞᱤᱠᱮᱥᱚᱱ" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "ᱴᱤᱡ" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "ᱴᱤᱯᱤᱡ" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "ᱴᱤᱯᱤᱡ" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "ᱴᱟᱲᱟᱝ" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "ᱴᱟᱲᱟᱝ" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "ᱫᱤᱱ" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "ᱫᱤᱱ" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "ᱢᱟᱦᱟᱸ" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "ᱢᱟᱦᱟᱸ" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "ᱥᱮᱨᱢᱟ" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "ᱥᱮᱨᱢᱟ" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "ᱮᱯ" + +#~| msgid "Power Off" +#~ msgid "_Power Off" +#~ msgstr "ᱯᱟᱣᱟᱨ ᱵᱚᱸᱫᱽ" + +#~ msgid "_Screenshot" +#~ msgstr "ᱥᱠᱨᱤᱱᱥᱚᱴ" + +#~ msgid "Number" +#~ msgstr "ᱱᱚᱢᱵᱚᱨ" + +#, c-format +#~ msgid "In %d day" +#~ msgid_plural "In %d days" +#~ msgstr[0] " %d ᱫᱤᱱ ᱨᱮ" +#~ msgstr[1] " %d ᱫᱤᱱ ᱨᱮ" +#~ msgstr[2] " %d ᱫᱤᱱ ᱨᱮ" + +#~ msgid "Lock Screen" +#~ msgstr "ᱠᱩᱞᱩᱯ ᱥᱠᱨᱤᱱ" + +#~ msgid "Logout" +#~ msgstr "ᱵᱟᱦᱨᱮ ᱛᱮ ᱚᱰᱚᱠ" diff --git a/po/sk.po b/po/sk.po new file mode 100644 index 000000000..541c421be --- /dev/null +++ b/po/sk.po @@ -0,0 +1,439 @@ +# Jaroslav Svoboda , 2018. #zanata +# Jaroslav Svoboda , 2019. #zanata +# Stanislav Jakúbek , 2019. #zanata +# Marek Lach Bc , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2022-02-23 08:10+0000\n" +"PO-Revision-Date: 2022-02-23 10:51+0100\n" +"Last-Translator: Marek Lach Bc \n" +"Language-Team: Slovak \n" +"Language: sk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Gtranslator 40.0\n" +"Plural-Forms: nplurals=3; plural=(n==1) ? 1 : (n>=2 && n<=4) ? 2 : 0\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/sm.puri.Phosh.desktop.in.in:4 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/sm.puri.Phosh.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Správa okien a spúšťanie aplikácií pre mobilné zariadenia" + +#: src/app-grid-button.c:507 +msgid "Application" +msgstr "Aplikácia" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Ukázať všetky aplikácie" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Ukázať iba aplikácie pripravené pre mobil" + +#: src/bt-info.c:92 src/feedbackinfo.c:51 src/rotateinfo.c:103 +msgid "On" +msgstr "Zapnutá" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Ukotvená" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Neukotvená" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Odhlásiť sa" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Vypnúť" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Reštartovať" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "" +msgstr[1] "" +msgstr[2] "" + +#: src/end-session-dialog.c:269 +msgid "Unknown application" +msgstr "Neznáma aplikácia" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:44 +msgid "Quiet" +msgstr "" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:49 +msgid "Silent" +msgstr "Tichý" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Povoliť '%s' prístup k informáciám o vašej polohe?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Áno" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Nie" + +#: src/lockscreen.c:163 src/ui/lockscreen.ui:270 +msgid "Enter Passcode" +msgstr "Zadajte heslo" + +#: src/lockscreen.c:362 +msgid "Checking…" +msgstr "Kontroluje sa..." + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:440 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:182 +msgid "Unknown Title" +msgstr "Neznámy názov" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:165 +msgid "Unknown Artist" +msgstr "Neznámy interprét" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Zabudovaný displej" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Neznámy" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Overovací spôsob wifi siete “%s” nieje podporovaný" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Zadajte heslo pre wifi sieť “%s”" + +#: src/notifications/mount-notification.c:122 +msgid "Open" +msgstr "Otvoriť" + +#: src/notifications/notification.c:383 src/notifications/notification.c:639 +msgid "Notification" +msgstr "Oboznámenie" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "r" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "r" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "teraz" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Cez %dy" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Skoro %dy" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "" + +#: src/polkit-auth-agent.c:228 +msgid "Authentication dialog was dismissed by the user" +msgstr "Autentifikačný dialóg bol zatvorený používateľom" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Heslo:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Nepodarilo sa to. Skúste to znova." + +#: src/rotateinfo.c:81 +msgid "Portrait" +msgstr "Na výšku" + +#: src/rotateinfo.c:84 +msgid "Landscape" +msgstr "Na šírku" + +#. Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) +#. Translators: Automatic screen orientation is off (locked/disabled) +#: src/rotateinfo.c:103 src/rotateinfo.c:186 +msgid "Off" +msgstr "Vypnutá" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Pre zatvorenie stlačte ESC" + +#: src/run-command-manager.c:94 +#, c-format +msgid "Running '%s' failed" +msgstr "Spustenie '%s' zlyhalo" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Heslá sa nezhodujú." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Heslo nemôže byť prázdne" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Svietidlo" + +#: src/ui/app-auth-prompt.ui:40 +msgid "Remember decision" +msgstr "Pamätať si voľbu" + +#: src/ui/app-auth-prompt.ui:53 src/ui/end-session-dialog.ui:53 +msgid "Cancel" +msgstr "Zrušiť" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Aplikácia" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Odobrať z _Obľúbených" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Pridať do _Obľúbených" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Hľadať aplikácie" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Niektoré aplikácie sú zaneprázdnené, alebo majú neuloženú prácu" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Používateľ:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Doména:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "Pri_pojiť" + +#: src/ui/lockscreen.ui:42 +msgid "Slide up to unlock" +msgstr "Ťahaním nahor odomknete" + +#: src/ui/lockscreen.ui:320 +msgid "Emergency" +msgstr "Pohotovosť" + +#: src/ui/lockscreen.ui:336 +msgid "Unlock" +msgstr "Odomknúť" + +#: src/ui/lockscreen.ui:376 +msgid "Back" +msgstr "Späť" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Vyžadované overenie" + +#: src/ui/network-auth-prompt.ui:40 +msgid "_Cancel" +msgstr "_Zrušiť" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "P_ripojiť" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Overiť" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Spustiť príkaz" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Potvrdiť:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Uzamknúť obrazovku" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Odhlásiť sa" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "" + +#. Translators: Refers to the cellular wireless network +#: src/wwaninfo.c:172 +msgid "Cellular" +msgstr "Mobilná sieť" diff --git a/po/sl.po b/po/sl.po new file mode 100644 index 000000000..c40032f52 --- /dev/null +++ b/po/sl.po @@ -0,0 +1,1257 @@ +# Slovenian translation for phosh. +# Copyright (C) 2021 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# FIRST AUTHOR , YEAR. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-04 21:09+0100\n" +"Last-Translator: Martin Srebotnjak \n" +"Language-Team: Slovenian \n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100>=3 && " +"n%100<=4 ? 2 : 3);\n" +"X-Generator: Poedit 3.8\n" +"X-Poedit-SourceCharset: ISO-8859-1\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Lupina telefona" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Upravljanje oken in zagon programja za mobilnike" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Ta seja vas prijavi v Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Hitra nastavitev za Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Prepreči, da bi seja ostala nedejavna" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Koledar" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Preprost programček za koledar" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Hitra nastavitev temnega načina / barvne sheme" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Preklopi temni način" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Podatki za primere v sili" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Pokaži informacije za nujne primere in stike" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Škatla zaganjalnikov" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Na zaklenjeni zaslon dodajte zaganjalnike. Ta vstavek je poskusen." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Hitra nastavitev lokacije" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Vklop/izklop lokacijskih storitev" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Predvajalniki predstavnosti" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Sledi predvajalnikom predstavnosti, ki so trenutno zagnani" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Hitra nastavitev za prenos podatkov" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Vklopi/izklopi prenos podatkov v mobilnem omrežju" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Hitra nastavitev nočne osvetlitve" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Vklop/izklopi nočno osvetlitev" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Hitra nastavitev Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Enostavni časovnik Pomodoro" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Škatla z vstopnicami" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Pokaži PDF-je na zaklenjenem zaslonu. Ta vstavek je poskusen." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Prihodnji dogodki" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Pokaži prihodnje dogodke koledarja" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Dodaj v mapo" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Ustvari novo mapo" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Program" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Pokaži vse programe" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Pokaži le na nabilnih napravah podprte programe" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Gum za glasnost lupine telefona" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Baterija %.0f %%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Vključeno" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Ni najdenih naprav Bluetooth za priključitev" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth je onemogočen" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Neznan klicatelj" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Brezžično omrežje »%s« uporablja prestrezni portal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Brezžično omrežje uporablja prestrezni portal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Vpiši se v brezžično omrežje" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Sidrano" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Odsidrano" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "V redu" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Klica v sili ni možno izvesti" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Notranja napaka" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Odjavi" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s se bo samodejno odjavil čez %d sekundo." +msgstr[1] "%s se bo samodejno odjavil čez %d sekundi." +msgstr[2] "%s se bo samodejno odjavil čez %d sekunde." +msgstr[3] "%s se bo samodejno odjavil čez %d sekund." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Ugasni" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistem se bo samodejno izklopil v %d sekundi." +msgstr[1] "Sistem se bo samodejno izklopil v %d sekundah." +msgstr[2] "Sistem se bo samodejno izklopil v %d sekundah." +msgstr[3] "Sistem se bo samodejno izklopil v %d sekundah." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Ponovno zaženi" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistem se bo samodejno ponovno zagnal čez %d sekundo." +msgstr[1] "Sistem se bo samodejno ponovno zagnal čez %d sekundi." +msgstr[2] "Sistem se bo samodejno ponovno zagnal čez %d sekunde." +msgstr[3] "Sistem se bo samodejno ponovno zagnal čez %d sekund." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Tiho" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Tiho" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Vključeno" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Želite dovoliti »%s« dostop do podatkov o vaši lokaciji?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Geolokacija" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Da" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Ne" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Vnesite geslo" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Preverjanje …" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Zaslonska slika shranjena v »%s«" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Posnetka zaslona ni bilo mogoče shraniti" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Posnetek zaslona" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Zaslonske slike" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Zaslonska slika iz %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Neznan naslov" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Neznan izvajalec" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Vgrajen zaslon" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Neznano" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Vrsta preverjanja pristnosti omrežja Wi-Fi »%s« ni podprta" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Vnesite geslo za WiFi-omrežje »%s«" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Odpri" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Obvestilo" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "zdaj" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "< 30 s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "< 1 min" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~ 1 min" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d min" +msgstr[1] "%d min" +msgstr[2] "%d min" +msgstr[3] "%d min" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~ %d u" +msgstr[1] "~ %d u" +msgstr[2] "~ %d u" +msgstr[3] "~ %d u" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 dan" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d d" +msgstr[1] "%d d" +msgstr[2] "%d d" +msgstr[3] "%d d" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 mes." + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d mes" +msgstr[1] "%d mes" +msgstr[2] "%d mes" +msgstr[3] "%d mes" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d leto" +msgstr[1] "~%d leti" +msgstr[2] "~%d leta" +msgstr[3] "~%d let" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Prek %d letom" +msgstr[1] "Prek %d letoma" +msgstr[2] "Prek %d leti" +msgstr[3] "Prek %d leti" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Skoraj %d leto" +msgstr[1] "Skoraj %d leti" +msgstr[2] "Skoraj %d leta" +msgstr[3] "Skoraj %d let" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Uporabnik je opustil pogovorno okno za preverjanje pristnosti" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Geslo:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Žal to ne deluje. Poskusite znova." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Pokončno" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Ležeče" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Izključeno" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Vključeno" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Pritisnite ESC, da zaprete" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Zagon »%s« ni uspel" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Gesli se ne ujemata." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Geslo ne sme biti prazno." + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Svetilka" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Pomni odločitev" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Prekliči" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Odstrani iz pri_ljubljenih" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Dodaj med pri_ljubljene" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Pokaži po_drobnosti" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Odstrani namestitev" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Odstrani iz _mape" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Poišči program …" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Izhodne naprave" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Vhodne naprave" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Nastavitve zvoka" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Samodejna svetlost" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Nastavitve svetlosti" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Omogoči Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Nastavitve za Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Zapri pogovorno okno klica v sili" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "_Stiki v sili" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Pojdi na stran stikov v sili" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Pojdi nazaj na stran številčnice v sili" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Lastnik neznan" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Stiki v sili" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Stiki v sili niso na voljo." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Nekateri programčki so sredi dela ali nimajo shranjenega dela" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Povratne informacije" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Ne moti" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Nastavitve povratnih informacij" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Uporabnik:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domena:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Poveži" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Nazaj" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Oddrsnite navzgor, da odklenete" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Odkleni" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Zahtevana je overitev" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "Pre_kliči" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Poveži" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Overi" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "V pripravljenost" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Zakleni" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Klic v sili" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Zaženi ukaz" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Ni obvestil" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Obvestila" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Počisti vse" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Potrditev:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "Iz_klopi …" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "Ponovno _zaženi …" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_V stanje pripravljenosti …" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Odjavi …" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Nastavitve za Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %e. %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Vtičnik ni najden" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Vtičnika »%s« ni mogoče naložiti." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Naprave Wi-Fi ni bilo mogoče najti" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi je onemogočen" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Omogoči Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Dejavna vroča točka Wi-Fi" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Izklopi" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Ni dostopnih točk Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobilno" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh na Caffeinu" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Izključeno" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Vključeno" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Kofeinski časovniki" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Brez intervalov kofeina" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Nastavitve za Caffeine" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Brez časovne omejitve (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Možnosti hitrih nastavitev za Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Trajanje Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Upravljaj trajanje Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Dodajte ali odstranite intervale Caffeine po meri" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Dodaj interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Dodaj nov interval" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Dodaj" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Intervali hitrih nastavitev" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 min" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 ura" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Izberite interval" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Privzeti slog" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Temni način" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Svetli način" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Osebni podatki" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Datum rojstva" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Prednostni jezik" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Domači naslov" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Zdravstveni podatki" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Starost" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Krvna skupina" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Višina" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Teža" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alergije" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Zdravila in bolezni" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Druge informacije" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Nastavitve podatkov za primere v sili" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Opravljeno" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Ime lastnika" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Datum rojstva" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Prednostni jezik" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Starost" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Krvna skupina" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Višina" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Teža" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Zdravila in bolezni" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Dodaj stik" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Dodaj nov stik" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Ime stika" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relacija" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Številka stika" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Noben zaganjalnik ni nastavljen" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Zaganjalniki" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Lokacija vklopljena" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Lokacija izklopljena" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Brez zagnanih predvajalnikov predstavnosti" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Prenos podatkov v mobilnem omrežju je vklopljen" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Prenos podatkov v mobilnem omrežju je izklopljen" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Nočna osvetlitev vklopljena" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Nočna osvetlitev izklopljena" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Zagon Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Osredotočite se na opravilo %d minut" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Vzemite si odmor" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Do naslednjega Pomodora imate %d minut" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Časovnik Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro izklopljen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Nastavitve hitre nastavitve Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Tehnika Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Aktivno trajanje" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Trajanje osredotočene seje" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Trajanje odmora" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Trajanje odmora med sejami" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Zaženi ob odklepu" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Ali želite zagnati časovnik ob odklepanju zaslona" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Ni dokumentov za prikaz" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Vstopnice" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Odpri" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Izberite mapo" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Nastavitve škatle z vstopnicami" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Poti" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Nastavitve mape" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Kje Phosh išče vaše vstopnice" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Mapa z vstopnicami" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Danes" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Jutri" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x, %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Ni dogodkov" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Ves dan" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Konča se" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Neimenovan dogodek" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Ni dogodkov v naslednjih %d dneh" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Ni prihodnjih dogodkov" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Nastavitve prihodnjih dogodkov" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dni" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Datum od - do" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Število dni za prikazovanja dogodkov" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Faktor velikosti zaslona" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d %%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Dostopna točka Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Vklopi" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Dostopna točka je vklopljena" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Dostopna točka je izklopljena" diff --git a/po/sr.po b/po/sr.po new file mode 100644 index 000000000..24a9f2732 --- /dev/null +++ b/po/sr.po @@ -0,0 +1,713 @@ +# Serbian translation for phosh. +# Copyright ©2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# nikp123 , 2020. +# Марко М. Костић , 2020. +# Мирослав Николић , 2021-2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-03-07 10:28+0000\n" +"PO-Revision-Date: 2023-04-09 11:54+0200\n" +"Last-Translator: Мирослав Николић \n" +"Language-Team: Serbian \n" +"Language: sr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=4; plural=n==1? 3 : n%10==1 && n%100!=11 ? 0 : " +"n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2\n" +"X-Generator: Gtranslator 41.0\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Телефонска шкољка" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Управљање прозорима и покретање програма на мобилним уређајима" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Ова сесија вас пријављује у Phosh" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Календар" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Једноставан прозорчић календара" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Кутуија картица" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Приказује ПДФ на закључаном екрану. Овај прикључак је експериментални." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Будући догађаји" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Прикажи будуће догађаје у календару" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "Апликација" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "Прикажи све програме" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "Прикажи само мобилно пригодне програме" + +#: src/bt-info.c:92 +#| msgid "On" +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Укљ." + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "Блутут" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Усидрен" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Одсидрен" + +#: src/end-session-dialog.c:162 +msgid "Log Out" +msgstr "Одјави ме" + +#: src/end-session-dialog.c:165 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "„%s“ ће бити одјављен за %d секунду." +msgstr[1] "„%s“ ће бити одјављен за %d секунде." +msgstr[2] "„%s“ ће бити одјављен за %d секунди." +msgstr[3] "„%s“ ће бити одјављен за једну секунду." + +#: src/end-session-dialog.c:171 src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "Искључивање" + +#: src/end-session-dialog.c:172 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Рачунар ће се искључити за %d секунду." +msgstr[1] "Рачунар ће се искључити за %d секунде." +msgstr[2] "Рачунар ће се искључити за %d секунди." +msgstr[3] "Рачунар ће се искључити за једну секунду." + +#: src/end-session-dialog.c:178 src/ui/top-panel.ui:29 +msgid "Restart" +msgstr "Поново покрени" + +#: src/end-session-dialog.c:179 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Систем ће се поново покренути за %d секунду." +msgstr[1] "Систем ће се поново покренути за %d секунде." +msgstr[2] "Систем ће се поново покренути за %d секунди." +msgstr[3] "Систем ће се поново покренути за једну секунду." + +#: src/end-session-dialog.c:269 +msgid "Unknown application" +msgstr "Непознат програм" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Тихо" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Утишано" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +#| msgid "On" +msgctxt "feedback:enabled" +msgid "On" +msgstr "Укљ." + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Да дозволим да „%s“ приступи вашим подацима о месту?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Геолокација" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Да" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Не" + +#: src/lockscreen.c:169 src/ui/lockscreen.ui:232 +msgid "Enter Passcode" +msgstr "Унеси код" + +#: src/lockscreen.c:392 +msgid "Checking…" +msgstr "Проверавам…" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:322 src/ui/media-player.ui:182 +msgid "Unknown Title" +msgstr "Непознат наслов" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:330 src/ui/media-player.ui:165 +msgid "Unknown Artist" +msgstr "Непознат извођач" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "Уграђени екран" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "Непознато" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Тип пријављивања на бежичној мрежи „%s“ није подржан" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "Унесите лозинку бежичне мреже „%s“" + +#: src/notifications/mount-notification.c:122 +msgid "Open" +msgstr "Отвори" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "Обавештење" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "с" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "м" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "м" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "ч" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "ч" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "д" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "д" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "мес." + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "мес." + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "г" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "г" + +#: src/notifications/timestamp-label.c:121 +msgid "now" +msgstr "сада" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, c-format +msgid "Over %dy" +msgstr "Преко %dг" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, c-format +msgid "Almost %dy" +msgstr "Скоро %dг" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "%s%d%s" + +#: src/polkit-auth-agent.c:228 +msgid "Authentication dialog was dismissed by the user" +msgstr "Корисник је одбацио прозорче за потврђивање идентитета" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "Лозинка:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Нетачно. Покушајте поново." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Усправно" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Положено" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +#| msgid "Off" +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Искљ." + +#: src/rotateinfo.c:126 +#| msgid "On" +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Укљ." + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Притисните „ESC“ да затворите" + +#: src/run-command-manager.c:95 +#, c-format +msgid "Running '%s' failed" +msgstr "Покретање „%s“ није успело" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "Лозинке се не подударају." + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "Лозинка не може бити празна" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Бакља" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "Упамти одлуку" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +msgid "Cancel" +msgstr "Откажи" + +#: src/ui/app-auth-prompt.ui:71 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "У реду" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "Програм" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "Уклони из о_миљених" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "Додај у о_миљено" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "Прикажи _појединости" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "Претражи апликације…" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "Неки програми су заузети или имају несачувани рад" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "Корисник:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "Домен:" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "П_овежи се" + +#: src/ui/lockscreen.ui:39 src/ui/lockscreen.ui:340 +msgid "Back" +msgstr "Назад" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Превуци нагоре за откључавање" + +#: src/ui/lockscreen.ui:282 +msgid "Emergency" +msgstr "Хитни позив" + +#: src/ui/lockscreen.ui:298 +msgid "Unlock" +msgstr "Откључај" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +msgid "Authentication required" +msgstr "Потребно је потврђивање идентитета" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Cancel" +msgstr "_Откажи" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "П_овежи се" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "Потврди идентитет" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "Покрени наредбу" + +#: src/ui/settings.ui:319 +msgid "No notifications" +msgstr "Нема обавештења" + +#: src/ui/settings.ui:359 +msgid "Clear all" +msgstr "Очисти све" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "Потврди:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "Закључавање екрана" + +#: src/ui/top-panel.ui:22 +msgid "Logout" +msgstr "Одјава" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +msgid "%A, %B %-e" +msgstr "%A, %d. %B" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "ВПН" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Нисам нашао прикључак" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Не могу да учитам прикључак „%s“." + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Бежична" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Мобилна" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "Лични подаци" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "Датум рођења" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "Жељени језик" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "Кућна адреса" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "Здравствени подаци" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "Године" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "Крвна група" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "Висина" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "Тежина" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "Алергије" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "Лекови & здравствено стање" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "Остали подаци" + +#: plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +#| msgid "Emergency" +msgid "Emergency Contacts" +msgstr "Хитни позиви" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "Инфо поставке хитних" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "Готово" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "Име власника" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "Лекови и стање здравља" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "Додај контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "Додај нови контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "Додај" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:312 +msgid "New Contact Name" +msgstr "Име новог контакта" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:326 +#| msgid "Geolocation" +msgid "Relationship" +msgstr "Сродство" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:339 +msgid "Number" +msgstr "Број" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Нема докумената за приказивање" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "Картице" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "Изабери фасциклу" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +#| msgid "Open" +msgid "_Open" +msgstr "_Отвори" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Поставке кутије картица" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "Путање" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "Поставке фасцикле" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "Где ће Phosh тражити ваше картице" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "Фасцикла картица" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Данас" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Сутра" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "За %d дан" +msgstr[1] "За %d дана" +msgstr[2] "За %d дана" +msgstr[3] "За %d дан" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Нема догађаја" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Читав дан" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Крај" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "Неименовани догађај" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Прикажи само прилагодљиве програме" + +#~ msgid "%d.%m.%y" +#~ msgstr "%d.%m.%y" + +#~ msgid "Suspend" +#~ msgstr "Обустави" diff --git a/po/sv.po b/po/sv.po new file mode 100644 index 000000000..92700b2ee --- /dev/null +++ b/po/sv.po @@ -0,0 +1,1323 @@ +# Ludwig Johnson , 2018. #zanata +# Nami , 2019. #zanata +# Anders Jonsson , 2020, 2021, 2022, 2023, 2024, 2025, 2026. +# Luna Jernberg , 2021, 2022, 2023. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-05 22:46+0100\n" +"Last-Translator: Anders Jonsson \n" +"Language-Team: Swedish \n" +"Language: sv\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Phone Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Fönsterhantering och programstartare för mobil" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Denna session loggar in dig i Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Koffeinsnabbinställning" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Förhindra sessionen från att bli inaktiv" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Kalender" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "En enkel kalenderkomponent" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Snabbinställning för mörkt läge / färgschema" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Växla mörkt läge" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Nödinformation" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Visa nödinformation och nödkontakter" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Programstartarbox" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Lägger till programstartare på låsskärmen. Denna insticksmodul är " +"experimentell." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Snabbinställning för plats" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Aktivera/inaktivera platstjänster" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Mediaspelare" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Följ vilka mediaspelare som körs" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Snabbinställning för mobildata" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Aktivera/inaktivera mobildata" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Snabbinställning för nattbelysning" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Aktivera/inaktivera nattbelysning" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Snabbinställning för Pomodoro" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Enkel Pomodoroklocka" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Biljettstånd" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Visa PDF-filer på låsskärmen. Denna insticksmodul är experimentell." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Kommande händelser" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Visa kommande kalenderhändelser" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Lägg till i mapp" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Skapa ny mapp" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Program" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Visa alla appar" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Visa endast mobilanpassade appar" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Volymkontroll för Phone Shell" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Batteri %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "På" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Inga Bluetooth-enheter att ansluta till hittades" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth inaktiverat" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Okänd uppringare" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Wi-fi-nätverket ”%s” använder en upplåsningsportal" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wi-fi-nätverket använder en upplåsningsportal" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Logga in på wi-fi-nätverk" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Dockad" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Ej dockad" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "OK" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Det går inte att ringa nödsamtal" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Internt fel" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Logga ut" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s kommer att loggas ut automatiskt om %d sekund." +msgstr[1] "%s kommer att loggas ut automatiskt om %d sekunder." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Stäng av" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Systemet kommer att stängas av automatiskt om %d sekund." +msgstr[1] "Systemet kommer att stängas av automatiskt om %d sekunder." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Starta om" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Systemet kommer att startas om automatiskt om %d sekund." +msgstr[1] "Systemet kommer att startas om automatiskt om %d sekunder." + +# Inget ljud, möjligen vibration +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Ljudlös" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Tyst" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "På" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Ge ”%s” tillgång till din platsinformation?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Platsbestämning" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Ja" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Nej" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Ange lösenord" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Kontrollerar…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Skärmbild sparad till ”%s”" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Misslyckades med att spara skärmbild" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Skärmbild" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Skärmbilder" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Skärmbild från %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Okänd titel" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Okänd artist" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Inbyggd skärm" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Okänd" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Autentiseringstyp för Wi-Fi-nätverket ”%s” stöds ej" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Ange lösenord för Wi-Fi-nätverket ”%s”" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Öppna" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Avisering" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "nu" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dh" +msgstr[1] "~%dh" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1d" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1må" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dmå" +msgstr[1] "%dmå" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dår" +msgstr[1] "~%dår" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Över %dår" +msgstr[1] "Över %dår" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Nästan %dår" +msgstr[1] "Nästan %dår" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Autentiseringsdialogen avvisades av användaren" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Lösenord:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Tyvärr, det fungerade inte. Var god försök igen." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Stående" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Liggande" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Av" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "På" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Tryck Esc för att stänga" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Körning av ”%s” misslyckades" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Lösenorden matchar inte." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Lösenordet får inte vara tomt" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Ficklampa" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Kom ihåg beslut" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Avbryt" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Ta bort från _favoriter" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Lägg till i _favoriter" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Visa _detaljer" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Avinstallera" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "_Ta bort från mapp" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Sök appar…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Utgångsenheter" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Ingångsenheter" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Ljudinställningar" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Automatisk ljusstyrka" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Ljusstyrkeinställningar" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Aktivera Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth-inställningar" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Stäng nödsamtalsdialogrutan" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Nöd_kontakter" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Gå till sidan för nödkontakter" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Gå tillbaka till nöduppringningssidan" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Ägare okänd" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Nödkontakter" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Inga nödkontakter tillgängliga." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Några program är upptagna eller innehåller icke sparat arbete" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Återkoppling" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Stör ej" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Återkopplingsinställningar" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Användare:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Domän:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "A_nslut" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Bakåt" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Svep uppåt för att låsa upp" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Lås upp" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autentisering krävs" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "A_vbryt" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "A_nslut" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Godkänn" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Försätt i vänteläge" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Lås" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Nödläge" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Kör kommando" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Inga aviseringar" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Aviseringar" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Rensa alla" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Bekräfta:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "S_täng av…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "Starta _om…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Försätt i vänteläge…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Logga ut…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi-inställningar" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-e %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Insticksmodul hittades inte" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Insticksmodulen ”%s” kunde inte läsas in." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Ingen Wi-Fi-enhet hittades" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi inaktiverat" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "A_ktivera Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi-surfzon aktiv" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Stäng av" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Inga Wi-Fi-surfzoner" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Mobilt nätverk" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh på koffein" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Av" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "På" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Koffeintidtagare" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Inga koffeinintervall" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Koffeininställningar" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Ingen tidsgräns (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Koffeinsnabbinställning" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Koffeinlängd" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Hantera koffeinlängd" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Lägg till eller ta bort anpassade koffeinintervall" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Lägg till intervall" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Lägg till nytt intervall" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Lägg till" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Snabbstartsintervall" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 m" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 h" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Välj intervall" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Standardstil" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Mörkt läge" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Ljust läge" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Personlig information" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Födelsedatum" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Föredraget språk" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Hemadress" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Medicinsk information" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Ålder" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Blodgrupp" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Längd" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Vikt" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergier" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Medicin och diagnoser" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Annan information" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Inställningar för nödinformation" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Färdig" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "Ägarens _namn" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Födelse_datum" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Föredraget språk" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Ålde_r" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Blodgrupp" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Längd" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Vikt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Medicin och diagnoser" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Lägg till kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Lägg till ny kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "Namn på _kontakt" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Relation" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Kontaktens nummer" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Inga programstartare konfigurerade" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Programstartare" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Plats på" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Plats av" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Inga mediaspelare körs" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Mobildata på" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Mobildata av" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Nattbelysning på" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Nattbelysning av" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodorostart" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Fokusera på din uppgift i %d minuter" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Ta en rast" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Du har %d minuter till nästa Pomodoro" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Pomodoroklocka" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro av" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Snabbinställningar för Pomodoro" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoroteknik" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Aktiv längd" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Längd på fokussessionen" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Längd på rast" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Längd på rasten mellan sessioner" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "_Starta vid upplåsning" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Huruvida tidtagaren ska startas då skärmen låses upp" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Inga dokument att visa" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Biljetter" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "Ö_ppna" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Välj mapp" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Inställningar för biljettstånd" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Sökvägar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Mappinställningar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Var Phosh söker dina biljetter" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Biljettmapp" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "I dag" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "I morgon" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Inga händelser" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Hela dagen" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Slutar" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Namnlös händelse" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Inga händelser för de närmaste %d dagarna" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Inga kommande händelser" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Inställningar för kommande händelser" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Dagar" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Datumintervall" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Antal dagar att visa händelser för" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Skärmskalning" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Wi-Fi-surfzon" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Slå på" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Surfzon på" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Surfzon av" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "Om %u dag" +#~ msgstr[1] "Om %u dagar" + +#, c-format +#~ msgid "%i more" +#~ msgstr "%i till" + +#~ msgid "Number" +#~ msgstr "Nummer" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Skärmbild kopierad till urklipp" + +#~ msgid "Scan" +#~ msgstr "Sök av" + +#~ msgid "_Power Off" +#~ msgstr "S_täng av" + +#~ msgid "_Screenshot" +#~ msgstr "_Skärmbild" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "s" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "m" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "h" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "d" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "mån" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "mån" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "å" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "å" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "App" + +#~ msgid "Unknown application" +#~ msgstr "Okänt program" + +#~ msgid "Lock Screen" +#~ msgstr "Lås skärm" + +#~ msgid "Logout" +#~ msgstr "Logga ut" diff --git a/po/tr.po b/po/tr.po new file mode 100644 index 000000000..a78492ac4 --- /dev/null +++ b/po/tr.po @@ -0,0 +1,1226 @@ +# Turkish translation of sm.puri.Phosh. +# Copyright (C) 2019-2025 sm.puri.Phosh's copyright holder +# This file is distributed under the same license as the sm.puri.Phosh package. +# +# Sabri Ünal , 2023, 2024. +# Emin Tufan Çetin , 2019-2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-06 00:18+0300\n" +"Last-Translator: Sabri Ünal \n" +"Language-Team: Turkish \n" +"Language: tr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=1; plural=0;\n" +"X-Generator: Poedit 3.8\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Telefon Kabuğu" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Mobil için pencere yönetimi ve uygulama başlatımı" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Bu oturum sizi Phosh’a sokar" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Kafein Hızlı Ayarı" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Oturumun boşta kalmasını önle" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Takvim" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Basit takvim parçacığı" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Koyu Kip / Renk Düzeni Hızlı Ayarı" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Koyu kipi aç/kapat" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Acil Durum Bilgisi" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Acil durum bilgilerini ve kişilerini göster" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Başlatıcı Kutusu" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "Kilit ekranına başlatıcılar ekle. Bu uzantı deneyseldir." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Konum Hızlı Ayarı" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Konum hizmetlerini aç/kapat" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Ortam Oynatıcılar" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Şu anda çalışan ortam oynatıcıları izle" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Mobil Veri Hızlı Ayarı" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Mobil veriyi aç/kapat" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Gece Işığı Hızlı Ayarı" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Gece ışığını aç/kapat" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro Hızlı Ayarı" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Basit Pomodoro Zamanlayıcı" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Bilet Kutusu" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Kilit ekranında PDF göster. Bu uzantı deneyseldir." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Yaklaşan Olaylar" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Yaklaşan olayları göster" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Klasöre Ekle" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Yeni klasör oluştur" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Uygulama" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Tüm Uygulamaları Göster" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Yalnızca Mobil Uyumluları Göster" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Telefon Kabuğu Ses Denetimi" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Pil %%%.0f" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Açık" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Bağlanabilir Bluetooth Aygıtı bulunamadı" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth devre dışı" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Bilinmeyen arayan" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "'%s' Wi-Fi ağı oturum kapısı kullanıyor" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wi-Fi ağı oturum kapısı kullanıyor" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Wi-Fi ağında oturum aç" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Yuvada" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Yuvada Değil" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Tamam" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Acil durum araması yapılamadı" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "İç hata" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Oturumu Kapat" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s oturumu %d saniyede kendiliğinden kapanacak." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Gücü Kapat" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Sistem %d saniyede kendiliğinden kapanacak." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Yeniden Başlat" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Sistem %d saniyede kendiliğinden yeniden başlayacak." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Titreşim" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Sessiz" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Açık" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "'%s' konum bilginize erişebilsin mi?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Coğrafi Konum" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Evet" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Hayır" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Parolayı Gir" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Denetleniyor…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Ekran görüntüsü '%s' olarak kaydedildi" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Ekran görüntüsü kaydedilemedi" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Ekran Görüntüsü" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Ekran Görüntüleri" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Ekran görüntüsü %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Bilinmeyen Başlık" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Bilinmeyen Sanatçı" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "İç ekran" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Bilinmeyen" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "“%s” kablosuz ağının kimlik doğrulama türü desteklenmiyor" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "“%s” kablosuz ağı için parola gir" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Aç" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Bildirim" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "şimdi" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30 sn" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1 dk" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1 dk" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%d dk" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%d sa" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1 gün" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%d gün" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 ay" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%d ay" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%d yıl" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%d yılı aşkın" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Neredeyse %d yıl" + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Kullanıcı, kimlik doğrulama kutusunu görmezden geldi" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Parola:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Üzgünüz, işe yaramadı. Lütfen yeniden deneyin." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Dikey" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Yatay" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "Kapalı" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Açık" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "ESC’ye basarak kapat" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "'%s' çalıştırılamadı" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Parolalar eşleşmiyor." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Parola boş olamaz" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Fener" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Kararı anımsa" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "İptal" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "_Gözdelerden kaldır" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "_Gözdelere ekle" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "_Ayrıntıları Göster" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Kaldır" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Klasörden _Kaldır" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Uygulama ara…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Çıktı Aygıtları" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Girdi Aygıtları" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Ses Ayarları" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Kendiliğinden Parlaklık" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Parlaklık Ayarları" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Bluetooth’u Etkinleştir" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth Ayarları" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Acil durum arama penceresini kapat" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Acil Durumda _Ulaşılacaklar" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Acil durumda ulaşılacaklar sayfasına git" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Acil durum tuşlama sayfasına geri git" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Sahip bilinmiyor" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Acil Durumda Ulaşılacaklar" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Acil durumda ulaşılacak kişi yok." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Bazı uygulamalar meşgul ya da kaydedilmemiş verisi var" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Geri Bildirim" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Rahatsız etme" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Geri Bildirim Ayarları" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Kullanıcı:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Etki Alanı:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "_Bağlan" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Geri" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Açmak için kaldır" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Kilidi Aç" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Kimlik doğrulama gerekli" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "İ_ptal" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "_Bağlan" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Kimlik Doğrula" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Askıya Al" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Kilitle" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Acil" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Komut Çalıştır" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Bildirim yok" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Bildirimler" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Tümünü temizle" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Doğrula:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "_Gücü Kapat…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "_Yeniden Başlat…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Askıya Al…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "_Oturumu Kapat…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Kablosuz" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Kablosuz Ayarları" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Uzantı bulunamadı" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "'%s' uzantısı yüklenemedi." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Kablosuz Aygıtı Bulunamadı" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Kablosuz Devre Dışı" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Kablosuz Bağlantıyı Etkinleştir" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Kablosuz Erişim Noktası Etkin" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Kapat" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Kablosuz Erişim Noktası Yok" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Hücresel" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Kafeinli Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "Kapalı" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Açık" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Kafein zamanlayıcıları" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Kafein aralıkları yok" + +#: plugins/caffeine-quick-setting/qs.ui:55 +msgid "Caffeine Settings" +msgstr "Kafein Ayarları" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Zaman aşımı yok (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Kafein Hızlı Ayar Tercihleri" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Kafein Süresi" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Kafein Süresini Yönet" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Özel kafein aralıkları ekle ya da kaldır" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Aralık ekle" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Yeni Aralık Ekle" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Ekle" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Hızlı Başlangıç Aralıkları" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 dk." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 dk." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 dk." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 sa." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Aralık Seç" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Öntanımlı biçem" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Koyu kip" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Açık kip" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Kişisel Bilgiler" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Doğum Tarihi" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Yeğlenen Dil" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Ev Adresi" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Tıbbi Bilgiler" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Yaş" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Kan Grubu" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Boy" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Kilo" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Alerjiler" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "İlaçlar ve Koşullar" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Diğer Bilgiler" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Acil Durum Bilgi Tercihleri" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Bitti" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Sahip Adı" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Doğum _Tarihi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Yeğlenen Dil" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Y_aş" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Kan Grubu" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Boy" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "Kil_o" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "İlaçlar ve Koşullar" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Kişi Ekle" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Yeni Kişi Ekle" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Kişi Adı" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "İlişki" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "İletişim _Numarası" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Başlatıcı yapılandırılmadı" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Başlatıcılar" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Konum Açık" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Konum Kapalı" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Çalışan ortam oynatıcı yok" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Mobil Veri Açık" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Mobil Veri Kapalı" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Gece Işığı Açık" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Gece Işığı Kapalı" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Pomodoro başlangıcı" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "%d dakika göreve odaklan" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Mola ver" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Sonraki Pomodoroʼya %d dakika var" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Pomodoro Zamanlayıcı" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro Kapalı" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro Hızlı Ayar Tercihleri" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro Tekniği" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Etkin Süre" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Odaklanma oturumunun süresi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Mola Süresi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Oturumlar arasındaki mola süresi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "Kilidi açınca _başlat" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Ekran kilidini açınca zamanlayıcının başlatılması" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Gösterilecek belge yok" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Biletler" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Aç" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Klasör Seç" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Bilet Kutusu Tercihleri" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Yollar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Klasör Ayarları" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Phosh, biletlerinize nerede bakacak" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Bilet Klasörü" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Bugün" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Yarın" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Olay yok" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%3$p %1$l:%2$M" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Tüm gün" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Bitiş" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Adsız olay" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Sonraki %d gün için olay yok" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Yaklaşan olay yok" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Yaklaşan Olaylar Tercihleri" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Gün" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Tarih Aralığı" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Olayların gösterileceği gün sayısı" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Monitör ölçeği" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%%%d" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Kablosuz Erişim Noktası" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Aç" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Erişim Noktası Açık" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Erişim Noktası Kapalı" diff --git a/po/uk.po b/po/uk.po new file mode 100644 index 000000000..38042931c --- /dev/null +++ b/po/uk.po @@ -0,0 +1,1340 @@ +# Translation of Phosh into Ukrainian +# Copyright (C) 2020 phosh developers +# This file is distributed under the same license as the phosh package. +# +# Roman Riabenko , 2020. #zanata. +# Yuri Chornoivan , 2020, 2021, 2022, 2023, 2024, 2025, 2026. +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2026-02-04 11:02+0000\n" +"PO-Revision-Date: 2026-02-04 22:09+0200\n" +"Last-Translator: Yuri Chornoivan \n" +"Language-Team: Ukrainian \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Lokalize 23.04.3\n" +"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n" +"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n" + +#: data/mobi.phosh.Shell.desktop.in.in:3 data/wayland-sessions/phosh.desktop:3 +msgid "Phone Shell" +msgstr "Інтерфейс користувача телефону" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Window management and application launching for mobile" +msgstr "Керування вікнами і запуск програм для мобільних" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:3 data/wayland-sessions/phosh.desktop:2 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:4 +msgid "This session logs you into Phosh" +msgstr "Це — сеанс входу до Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:4 +msgid "Caffeine Quick Setting" +msgstr "Швидке налаштовування Caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:6 +msgid "Prevent the session from going idle" +msgstr "Запобігання переведення сеансу у режим бездіяльності" + +#: plugins/calendar/calendar.desktop.in.in:4 +msgid "Calendar" +msgstr "Календар" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "Простий віджет календаря" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:4 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Швидке налаштовування темного режиму або схеми кольорів" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:6 +msgid "Toggle dark mode" +msgstr "Перемкнути темний режим" + +#: plugins/emergency-info/emergency-info.desktop.in.in:4 +msgid "Emergency Info" +msgstr "Критичні відомості" + +#: plugins/emergency-info/emergency-info.desktop.in.in:6 +msgid "Show emergency information and contacts" +msgstr "Показ критичних відомостей і контактів" + +#: plugins/launcher-box/launcher-box.desktop.in.in:3 +#: plugins/launcher-box/launcher-box.ui:17 +msgid "Launcher Box" +msgstr "Панель засобів запуску" + +#: plugins/launcher-box/launcher-box.desktop.in.in:5 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Додає кнопки запуску на екран блокування. Цей додаток є тестовим." + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:4 +msgid "Location Quick Setting" +msgstr "Швидке налаштовування місця" + +#: plugins/location-quick-setting/location-quick-setting.desktop.in.in:6 +msgid "Toggle location services on/off" +msgstr "Увімкнути або вимкнути служби місця перебування" + +#: plugins/media-players/media-players.desktop.in.in:3 +#: plugins/media-players/media-players.ui:19 +#: plugins/media-players/media-players.ui:35 +msgid "Media Players" +msgstr "Програвачі" + +#: plugins/media-players/media-players.desktop.in.in:5 +msgid "Track currently running media players" +msgstr "Стежити за поточним запущеними програвачами" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:4 +msgid "Mobile Data Quick Setting" +msgstr "Швидке налаштовування мобільних даних" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:6 +msgid "Toggle mobile data on/off" +msgstr "Увімкнути або вимкнути мобільні дані" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:4 +msgid "Night Light Quick Setting" +msgstr "Швидке налаштовування нічних кольорів" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:6 +msgid "Toggle night light on/off" +msgstr "Увімкнути або вимкнути нічний режим" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:5 +msgid "Pomodoro Quick Setting" +msgstr "Швидке налаштовування помідора" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:7 +msgid "Simple Pomodoro Timer" +msgstr "Простий таймер-помідор" + +#: plugins/ticket-box/ticket-box.desktop.in.in:3 +#: plugins/ticket-box/ticket-box.ui:17 +msgid "Ticket Box" +msgstr "Скринька з квитками" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "Показ PDF на екрані блокування. Цей додаток є тестовим." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:3 +msgid "Upcoming Events" +msgstr "Майбутні події" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "Показати майбутні події календаря" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "Додати до теки" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Створити теку" + +#: src/app-grid-button.c:694 src/app-grid-button.c:748 +msgid "Application" +msgstr "Програма" + +#: src/app-grid.c:264 +msgid "Show All Apps" +msgstr "Показати усі програми" + +#: src/app-grid.c:267 +msgid "Show Only Mobile Friendly Apps" +msgstr "Показати лише програми для мобільних" + +#: src/audio-manager.c:74 +msgid "Phone Shell Volume Control" +msgstr "Керування гучністю оболонки телефону" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Акумулятор %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "❙" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Не знайдено придатних до з'єднання пристроїв Bluetooth" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth вимкнено" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Невідомий абонент" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "Мережа Wi-Fi «%s» використовує портал із захопленням даних" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Мережа Wi-Fi використовує портал із захопленням даних" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Реєстрація у мережі Wi–Fi" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Зафіксовано" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Від'єднано" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:18 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Гаразд" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Не вдалося розмістити екстрений виклик" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Внутрішня помилка" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Вийти" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "Вихід з системи %s буде здійснено автоматично за %d секунду." +msgstr[1] "Вихід з системи %s буде здійснено автоматично за %d секунди." +msgstr[2] "Вихід з системи %s буде здійснено автоматично за %d секунд." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:72 +msgid "Power Off" +msgstr "Вимкнути" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Система автоматично вимкнеться за %d секунду." +msgstr[1] "Система автоматично вимкнеться за %d секунди." +msgstr[2] "Система автоматично вимкнеться за %d секунд." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Перезапустити" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Система автоматично перезапуститься за %d секунду." +msgstr[1] "Система автоматично перезапуститься за %d секунди." +msgstr[2] "Система автоматично перезапуститься за %d секунд." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Тихо" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Беззвучно" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "❙" + +#: src/location-manager.c:266 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "Дозволити «%s» доступ до даних щодо вашого місця перебування?" + +#: src/location-manager.c:271 +msgid "Geolocation" +msgstr "Геопозиціювання" + +#: src/location-manager.c:272 +msgid "Yes" +msgstr "Так" + +#: src/location-manager.c:272 +msgid "No" +msgstr "Ні" + +#. give visual feedback on error +#: src/lockscreen.c:396 src/ui/lockscreen.ui:282 +msgid "Enter Passcode" +msgstr "Уведіть пароль" + +#: src/lockscreen.c:1036 +msgid "Checking…" +msgstr "Звіряю…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Знімок збережено до «%s»" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Не вдалося зберегти знімок екрана" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:199 +msgid "Screenshot" +msgstr "Знімок вікна" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Знімки вікон" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "Знімок вікна %s%s.png" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:691 src/ui/media-player.ui:208 +msgid "Unknown Title" +msgstr "Невідома назва" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:699 src/ui/media-player.ui:196 +msgid "Unknown Artist" +msgstr "Невідомий виконавець" + +#: src/monitor-manager.c:129 +msgid "Built-in display" +msgstr "Вбудований дисплей" + +#: src/monitor-manager.c:147 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:154 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:163 +msgid "Unknown" +msgstr "Нетиповий" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "Підтримки типу розпізнавання у мережі Wi-Fi «%s» не передбачено" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "Введіть пароль до мережі Wi-Fi «%s»" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Відкрити" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1009 +msgid "Notification" +msgstr "Сповіщення" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "зараз" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30с" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1х" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1х" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dх" +msgstr[1] "%dх" +msgstr[2] "%dх" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dг" +msgstr[1] "~%dг" +msgstr[2] "~%dг" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1д" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dд" +msgstr[1] "%dд" +msgstr[2] "%dд" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1м" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%dм" +msgstr[1] "%dм" +msgstr[2] "%dм" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dр" +msgstr[1] "~%dр" +msgstr[2] "~%dр" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "Понад %dр" +msgstr[1] "Понад %dр" +msgstr[2] "Понад %dр" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Майже %dр." +msgstr[1] "Майже %dр." +msgstr[2] "Майже %dр." + +#: src/polkit-auth-agent.c:275 +msgid "Authentication dialog was dismissed by the user" +msgstr "Користувач перервав діалог автентифікації" + +#: src/polkit-auth-prompt.c:382 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:69 src/ui/polkit-auth-prompt.ui:44 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Пароль:" + +#: src/polkit-auth-prompt.c:429 +msgid "Sorry, that didn’t work. Please try again." +msgstr "На жаль, це не спрацювало. Будь ласка, спробуйте ще." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "Книжкова" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Альбомна" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "○" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "❙" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Натисніть клавішу «Esc», щоб закрити" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "Не вдалося запустити «%s»" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Паролі не збігаються." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Пароль не може бути порожнім" + +#: src/torch-info.c:84 +msgid "Torch" +msgstr "Факел" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Запам'ятати вибір" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Скасувати" + +#: src/ui/app-grid-button.ui:31 +msgid "Remove from _Favorites" +msgstr "Вилучити з _вибраних" + +#: src/ui/app-grid-button.ui:36 +msgid "Add to _Favorites" +msgstr "Додати до _вибраних" + +#: src/ui/app-grid-button.ui:41 +msgid "View _Details" +msgstr "Пере_глянути подробиці" + +#: src/ui/app-grid-button.ui:46 +msgid "Uninstall" +msgstr "Вилучити" + +#: src/ui/app-grid-button.ui:53 +msgid "_Remove from Folder" +msgstr "Вилучити з _теки" + +#: src/ui/app-grid.ui:26 +msgid "Search apps…" +msgstr "Шукати серед програм…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Пристрої відтворення" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Пристрої отримання" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Параметри звуку" + +#: src/ui/brightness-settings.ui:87 +msgid "Automatic Brightness" +msgstr "Автоматична яскравість" + +#: src/ui/brightness-settings.ui:120 +msgid "Brightness Settings" +msgstr "Параметри яскравості" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Увімкнути Bluetooth" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Параметри Bluetooth" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "Закрити вікно екстреного виклику" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "Екстрені _контакти" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "Перейти до сторінки екстрених контактів" + +#: src/ui/emergency-menu.ui:82 +msgid "Go back to the emergency dialpad page" +msgstr "Повернутися до сторінки набору екстреного виклику" + +#: src/ui/emergency-menu.ui:105 +msgid "Owner unknown" +msgstr "Невідомий власник" + +#: src/ui/emergency-menu.ui:123 plugins/emergency-info/emergency-info.ui:211 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Екстрені контакти" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "Немає доступних екстрених контактів." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Деякі програми зайняті або мають незбережені дані" + +#: src/ui/feedback-status-page.ui:6 +msgid "Feedback" +msgstr "Відгук" + +#: src/ui/feedback-status-page.ui:19 +msgid "Do not disturb" +msgstr "Не турбувати" + +#: src/ui/feedback-status-page.ui:53 +msgid "Feedback Settings" +msgstr "Параметри відгуку" + +#: src/ui/gtk-mount-prompt.ui:74 +msgid "User:" +msgstr "Користувач:" + +#: src/ui/gtk-mount-prompt.ui:96 +msgid "Domain:" +msgstr "Домен:" + +#: src/ui/gtk-mount-prompt.ui:128 +msgid "Co_nnect" +msgstr "З'єд_натися" + +#: src/ui/lockscreen.ui:43 src/ui/lockscreen.ui:245 +msgid "Back" +msgstr "Назад" + +#: src/ui/lockscreen.ui:95 +msgid "Slide up to unlock" +msgstr "Протягніть угору, щоб розблокувати" + +#: src/ui/lockscreen.ui:332 +msgid "Unlock" +msgstr "Розблокувати" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Слід пройти розпізнавання" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:75 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Скасувати" + +#: src/ui/network-auth-prompt.ui:50 +msgid "C_onnect" +msgstr "З'_єднатися" + +#: src/ui/polkit-auth-prompt.ui:96 +msgid "Authenticate" +msgstr "Автентифікуватися" + +#: src/ui/power-menu.ui:112 +msgid "Suspend" +msgstr "Призупинити" + +#: src/ui/power-menu.ui:158 +msgid "Lock" +msgstr "Замкнути" + +#: src/ui/power-menu.ui:240 +msgid "Emergency" +msgstr "Екстрений виклик" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Виконати команду" + +#: src/ui/settings.ui:121 +msgid "No notifications" +msgstr "Немає сповіщень" + +#: src/ui/settings.ui:150 +msgid "Notifications" +msgstr "Сповіщення" + +#: src/ui/settings.ui:159 +msgid "Clear all" +msgstr "Очистити все" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Підтвердіть:" + +#: src/ui/top-panel.ui:34 +msgid "_Power Off…" +msgstr "Ви_мкнути…" + +#: src/ui/top-panel.ui:61 +msgid "_Restart…" +msgstr "П_ерезапустити…" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "_Призупинити…" + +#: src/ui/top-panel.ui:115 +msgid "_Log Out…" +msgstr "Ви_йти…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#: src/ui/wifi-status-page.ui:89 +#: plugins/wifi-hotspot-quick-setting/status-page.ui:85 +msgid "Wi-Fi Settings" +msgstr "Параметри Wi-Fi" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:248 +msgid "%A, %B %-e" +msgstr "%A, %-d %B" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Додаток не знайдено" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "Не вдалося завантажити додаток «%s»." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Не знайдено жодного пристрою Wi-Fi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi вимкнено" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Увімкнути Wi-Fi" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Активна точка доступу Wi-Fi" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "Вимкнути" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Немає точок доступу Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Стільниковий" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:70 +msgid "Phosh on caffeine" +msgstr "Phosh на caffeine" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:245 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "○" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:250 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "❙" + +#: plugins/caffeine-quick-setting/qs.ui:15 +msgid "Caffeine timers" +msgstr "Таймери caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:38 +msgid "No caffeine intervals" +msgstr "Немає інтервалів caffeine" + +#: plugins/caffeine-quick-setting/qs.ui:55 +#| msgid "Caffeine Quick Setting" +msgid "Caffeine Settings" +msgstr "Параметри Caffeine" + +#: plugins/caffeine-quick-setting/prefs/caffeine-quick-setting-prefs.c:253 +#: plugins/caffeine-quick-setting/prefs/prefs.ui:171 +msgid "No timeout (∞)" +msgstr "Без часу очікування (∞)" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:7 +msgid "Caffeine Quick Setting Preferences" +msgstr "Параметри швидкого налаштовування Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:11 +msgid "Caffeine Duration" +msgstr "Тривалість Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:15 +msgid "Manage Caffeine Duration" +msgstr "Керування тривалістю Caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:16 +msgid "Add or remove custom caffeine intervals" +msgstr "Додати або вилучити нетипові інтервали caffeine" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:22 +msgid "Add interval" +msgstr "Додати інтервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:66 +msgid "Add New Interval" +msgstr "Додати новий інтервал" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Додати" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:115 +msgid "Quickstart Intervals" +msgstr "Інтервали швидкого запуску" + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:127 +msgid "5 m" +msgstr "5 хв." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:138 +msgid "15 m" +msgstr "15 хв." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:149 +msgid "30 m" +msgstr "30 хв." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:160 +msgid "1 h" +msgstr "1 год." + +#: plugins/caffeine-quick-setting/prefs/prefs.ui:188 +msgid "Choose Interval" +msgstr "Вибрати інтервал" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:34 +msgid "Default style" +msgstr "Типовий стиль" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:35 +msgid "Dark mode" +msgstr "Темний режим" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Light mode" +msgstr "Світлий режим" + +#: plugins/emergency-info/emergency-info.ui:43 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Особисті відомості" + +#: plugins/emergency-info/emergency-info.ui:51 +msgid "Date of Birth" +msgstr "Дата народження" + +#: plugins/emergency-info/emergency-info.ui:71 +msgid "Preferred Language" +msgstr "Бажана мова" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Домашня адреса" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Медичні відомості" + +#: plugins/emergency-info/emergency-info.ui:107 +msgid "Age" +msgstr "Вік" + +#: plugins/emergency-info/emergency-info.ui:127 +msgid "Blood Type" +msgstr "Група крові" + +#: plugins/emergency-info/emergency-info.ui:147 +msgid "Height" +msgstr "Зріст" + +#: plugins/emergency-info/emergency-info.ui:167 +msgid "Weight" +msgstr "Вага" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Алергії" + +#: plugins/emergency-info/emergency-info.ui:195 +msgid "Medications & Conditions" +msgstr "Препарати і стан здоров'я" + +#: plugins/emergency-info/emergency-info.ui:203 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Інші відомості" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Налаштовування критичних відомостей" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Виконано" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Ім'я власника" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "Д_ата народження" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Бажана мова" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "_Вік" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Група крові" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "_Зріст" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "Ва_га" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Препарати і стан здоров'я" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "Додати контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Додати новий контакт" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Назва контакту" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Зв'язок" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "Н_омер контакту" + +#: plugins/launcher-box/launcher-box.ui:18 +msgid "No launchers configured" +msgstr "Не налаштовано жодного засобу запуску" + +#: plugins/launcher-box/launcher-box.ui:33 +msgid "Launchers" +msgstr "Засоби запуску" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location On" +msgstr "Позиціювання увімкнено" + +#: plugins/location-quick-setting/location-quick-setting.c:65 +msgid "Location Off" +msgstr "Позиціювання вимкнено" + +#: plugins/media-players/media-players.ui:20 +msgid "No running media players" +msgstr "Немає запущених програвачів" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data On" +msgstr "Мобільні дані увімкнено" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:79 +msgid "Mobile Data Off" +msgstr "Мобільні дані вимкнено" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light On" +msgstr "Нічні кольори увімкнено" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:63 +msgid "Night Light Off" +msgstr "Нічні кольори вимкнено" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +msgid "Pomodoro start" +msgstr "Запуск помідора" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:73 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "Зосередитися на завданні на %d хвилин" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:78 +msgid "Take a break" +msgstr "Зробіть перерву" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:80 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "У вас %d хвилин до наступного Помідора" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:95 +msgid "Pomodoro Timer" +msgstr "Таймер-помідор" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:118 +#, c-format +msgid "Pomodoro Off" +msgstr "Вимикання помідора" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Налаштування швидких параметрів помідора" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Методика помідора" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "Тривалість _активності" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Тривалість сеансу зосередження" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "Тривалість _перерви" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Тривалість перерви між сеансами" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:44 +msgid "_Start on unlock" +msgstr "З_апускати при розблокуванні" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:45 +msgid "Whether to start the timer on screen unlock" +msgstr "Визначає, чи слід запускати відлік при розблокуванні екрана" + +#: plugins/ticket-box/ticket-box.ui:18 +msgid "No documents to display" +msgstr "Немає документів для показу" + +#: plugins/ticket-box/ticket-box.ui:82 +msgid "Tickets" +msgstr "Квитки" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "_Відкрити" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Виберіть теку" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Налаштування скриньки з квитками" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Шляхи" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Параметри тек" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Де Phosh має шукати ваші квитки" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Тека квитків" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Сьогодні" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Завтра" + +#. Translators: The current date and weekday for display in a calendar entry +#: plugins/upcoming-events/event-list.c:151 +#, c-format +msgid "%x %a" +msgstr "%x, %a" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Немає подій" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%H:%M" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Увесь день" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Завершується" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Подія без назви" + +#: plugins/upcoming-events/upcoming-events.c:408 +#, c-format +msgid "No events for the next %d days" +msgstr "Немає подій протягом наступних %d днів" + +#: plugins/upcoming-events/upcoming-events.ui:28 +msgid "No upcoming events" +msgstr "Записів майбутніх подій не виявлено" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Параметри майбутніх подій" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Дні" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Діапазон дат" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Кількість днів, для якої буде показано події" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Масштаби монітора" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:91 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:6 +msgid "Wi-Fi Hotspot" +msgstr "Хот-спот Wi-Fi" + +#: plugins/wifi-hotspot-quick-setting/status-page.ui:71 +msgid "Turn On" +msgstr "Увімкнути" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:80 +msgid "Hotspot On" +msgstr "Точку доступу увімкнено" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:82 +msgid "Hotspot Off" +msgstr "Точку доступу вимкнено" + +#, c-format +#~ msgid "In %u day" +#~ msgid_plural "In %u days" +#~ msgstr[0] "За %u день" +#~ msgstr[1] "За %u дні" +#~ msgstr[2] "За %u днів" + +#~ msgid "Add" +#~ msgstr "Додати" + +#~ msgid "Number" +#~ msgstr "Номер" + +#~ msgid "Screenshot copied to clipboard" +#~ msgstr "Знімок вікна скопійовано до буфера" + +#~ msgid "Scan" +#~ msgstr "Сканувати" + +#~ msgid "_Power Off" +#~ msgstr "Ви_мкнути" + +#~ msgid "_Screenshot" +#~ msgstr "З_німок вікна" + +#~ msgctxt "timestamp-suffix-seconds" +#~ msgid "s" +#~ msgstr "с" + +#~ msgctxt "timestamp-suffix-minute" +#~ msgid "m" +#~ msgstr "хв" + +#~ msgctxt "timestamp-suffix-minutes" +#~ msgid "m" +#~ msgstr "хв" + +#~ msgctxt "timestamp-suffix-hour" +#~ msgid "h" +#~ msgstr "г" + +#~ msgctxt "timestamp-suffix-hours" +#~ msgid "h" +#~ msgstr "г" + +#~ msgctxt "timestamp-suffix-day" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-days" +#~ msgid "d" +#~ msgstr "д" + +#~ msgctxt "timestamp-suffix-month" +#~ msgid "mo" +#~ msgstr "м" + +#~ msgctxt "timestamp-suffix-months" +#~ msgid "mos" +#~ msgstr "міс" + +#~ msgctxt "timestamp-suffix-year" +#~ msgid "y" +#~ msgstr "р" + +#~ msgctxt "timestamp-suffix-years" +#~ msgid "y" +#~ msgstr "р" + +#, c-format +#~ msgid "%s%d%s" +#~ msgstr "%s%d%s" + +#~ msgid "App" +#~ msgstr "Програма" + +#~ msgid "Unknown application" +#~ msgstr "Невідома програма" + +#~ msgid "Lock Screen" +#~ msgstr "Заблокувати екран" + +#~ msgid "Logout" +#~ msgstr "Вийти" + +#, c-format +#~ msgid "On %A" +#~ msgstr "%A" + +#~ msgid "Show only adaptive apps" +#~ msgstr "Показувати лише адаптивні програми" diff --git a/po/uz.po b/po/uz.po new file mode 100644 index 000000000..94a42cd26 --- /dev/null +++ b/po/uz.po @@ -0,0 +1,1100 @@ +# Uzbek (Latin) translation for phosh. +# Copyright (C) 2025 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Baxrom Raxmatov , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh main\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2025-04-02 02:41+0000\n" +"PO-Revision-Date: 2025-04-04 22:30+0500\n" +"Last-Translator: \n" +"Language-Team: Uzbek (Latin) \n" +"Language: uz\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"X-Generator: Poedit 3.6\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "Telefon qobig'i" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "Mobil uchun oynani boshqarish va ilovalarni ishga tushirish" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "Ushbu seans sizni Phosh tizimiga kiritadi" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:5 +msgid "Caffeine Quick Setting" +msgstr "Kofeinni tez sozlash" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.desktop.in.in:7 +msgid "Prevent the session from going idle" +msgstr "Seansning bo'sh qolishiga yo'l qo'ymaslik" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "Taqvim" + +#: plugins/calendar/calendar.desktop.in.in:7 +msgid "A simple calendar widget" +msgstr "Oddiy kalendar vidjeti" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:5 +msgid "Dark Mode / Color Scheme Quick Setting" +msgstr "Qorong'i rejim / Rang sxemasini tez sozlash" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.desktop.in.in:7 +msgid "Toggle dark mode" +msgstr "Qorong'i rejimga o'tish" + +#: plugins/emergency-info/emergency-info.desktop.in.in:5 +msgid "Emergency Info" +msgstr "Favqulodda ma'lumot" + +#: plugins/emergency-info/emergency-info.desktop.in.in:7 +msgid "Show emergency information and contacts" +msgstr "Favqulodda ma'lumotlar va kontaktlarni ko'rsatish" + +#: plugins/launcher-box/launcher-box.desktop.in.in:4 +#: plugins/launcher-box/launcher-box.ui:14 +msgid "Launcher Box" +msgstr "Ishga tushirish qutisi" + +#: plugins/launcher-box/launcher-box.desktop.in.in:6 +msgid "Add launchers to the lock screen. This plugin is experimental." +msgstr "" +"Bloklangan ekranga ishga tushirgichlarni qo'shing. Ushbu plagin " +"eksperimental hisoblanadi." + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:5 +msgid "Mobile Data Quick Setting" +msgstr "Mobil ma'lumotlarni tezkor sozlash" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.desktop.in.in:7 +msgid "Toggle mobile data on/off" +msgstr "Mobil maʼlumotlarni yoqish/oʻchirish" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:5 +msgid "Night Light Quick Setting" +msgstr "Tungi yorug'likni tez sozlash" + +#: plugins/night-light-quick-setting/night-light-quick-setting.desktop.in.in:7 +msgid "Toggle night light on/off" +msgstr "Tungi yorug'likni yoqish/o'chirish" + +#. Translators: "Pomodoro" is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:6 +msgid "Pomodoro Quick Setting" +msgstr "Pomodoro tezkor sozlash" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.desktop.in.in:8 +msgid "Simple Pomodoro Timer" +msgstr "Oddiy Pomodoro taymer" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "Chipta qutisi" + +#: plugins/ticket-box/ticket-box.desktop.in.in:6 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" +"Bloklangan ekranda PDF-fayllarni ko'rsatish. Ushbu plagin eksperimental " +"hisoblanadi." + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "Kelgusi voqealar" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:6 +msgid "Show upcoming calendar events" +msgstr "Kelgusi kalendar tadbirlarini ko'rsatish" + +#: src/app-grid-button.c:144 +msgid "Add to Folder" +msgstr "_Jild qo‘shish" + +#: src/app-grid-button.c:168 +msgid "Create new folder" +msgstr "Yangi jild yarating" + +#: src/app-grid-button.c:698 src/app-grid-button.c:754 +msgid "Application" +msgstr "Ilovani o'rnating (ilova identifikatori yordamida)" + +#: src/app-grid.c:261 +msgid "Show All Apps" +msgstr "Barcha ilovalarni ko'rsatish" + +#: src/app-grid.c:264 +msgid "Show Only Mobile Friendly Apps" +msgstr "Faqat mobil ilovalarni ko'rsatish" + +#. Translators: a battery level in percent +#: src/bt-device-row.c:81 +#, c-format +msgid "Battery %.0f%%" +msgstr "Batareya quvvati: %.0f%%" + +#: src/bt-info.c:95 src/ui/bt-status-page.ui:6 +msgid "Bluetooth" +msgstr "Bluetooth" + +#: src/bt-info.c:123 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "Yoniq" + +#: src/bt-status-page.c:123 +msgid "No connectable Bluetooth Devices found" +msgstr "Ulanadigan Bluetooth qurilmalari topilmadi" + +#: src/bt-status-page.c:127 +msgid "Bluetooth disabled" +msgstr "Bluetooth o'chirilgan" + +#: src/call-notification.c:63 +msgid "Unknown caller" +msgstr "Noma'lum qo'ng'iroq qiluvchi" + +#: src/connectivity-manager.c:114 +#, c-format +msgid "Wi-Fi network '%s' uses a captive portal" +msgstr "\"%s\" Wi-Fi tarmog'i tutqun portaldan foydalanadi" + +#: src/connectivity-manager.c:116 +msgid "The Wi-Fi network uses a captive portal" +msgstr "Wi-Fi tarmog'i tutqun portaldan foydalanadi" + +#: src/connectivity-manager.c:123 +msgid "Sign into Wi-Fi network" +msgstr "Wi-Fi tarmog'iga kiring" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "Docked" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "Ochilgan" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:64 +#: src/ui/cell-broadcast-prompt.ui:22 src/ui/end-session-dialog.ui:62 +msgid "Ok" +msgstr "Ok" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "Favqulodda qo‘ng‘iroqni amalga oshirib bo‘lmadi" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "Ichki xato" + +#: src/end-session-dialog.c:173 +msgid "Log Out" +msgstr "Chiqish" + +#: src/end-session-dialog.c:176 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "%s %d soniyadan so‘ng avtomatik ravishda chiqadi." +msgstr[1] "%s %d soniyada avtomatik ravishda chiqariladi." + +#: src/end-session-dialog.c:182 src/ui/power-menu.ui:69 +msgid "Power Off" +msgstr "O’chirish" + +#: src/end-session-dialog.c:183 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "Tizim %d soniyada avtomatik ravishda oʻchadi." +msgstr[1] "Tizim %d soniyada avtomatik ravishda oʻchiriladi." + +#: src/end-session-dialog.c:189 +msgid "Restart" +msgstr "Qayta ishga tushirish" + +#: src/end-session-dialog.c:190 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "Tizim %d soniyadan keyin avtomatik ravishda qayta ishga tushadi." +msgstr[1] "Tizim %d soniyada avtomatik ravishda qaytadan ishga tushiriladi." + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "Tinch" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "Jim" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "Yoniq" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "“%s” ilovasiga joylashuv maʼlumotlariga kirishiga ruxsat berilsinmi?" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "Geolokatsiya" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "Xa" + +#: src/location-manager.c:274 +msgid "No" +msgstr "Yo'q" + +#. give visual feedback on error +#: src/lockscreen.c:311 src/ui/lockscreen.ui:280 +msgid "Enter Passcode" +msgstr "Parolni kiriting" + +#: src/lockscreen.c:984 +msgid "Checking…" +msgstr "Tekshirilmoqda…" + +#. Translators: '%s' is the filename of a screenshot +#: src/screenshot-manager.c:238 +#, c-format +msgid "Screenshot saved to '%s'" +msgstr "Skrinshot “%s” jildiga saqlandi" + +#: src/screenshot-manager.c:240 +msgid "Failed to save screenshot" +msgstr "Skrinshot saqlanmadi" + +#: src/screenshot-manager.c:244 src/ui/power-menu.ui:187 +msgid "Screenshot" +msgstr "Skrinshot" + +#. Translators: name of the folder beneath ~/Pictures used to store screenshots +#: src/screenshot-manager.c:554 +msgid "Screenshots" +msgstr "Skrinshotlar" + +#. Translators: Name of a screenshot file. The first '%s' is a timestamp +#. * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case +#. * the file already exists like '-3' +#: src/screenshot-manager.c:574 +#, c-format +msgid "Screenshot from %s%s.png" +msgstr "%s%s.png dan skrinshot" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:493 src/ui/media-player.ui:211 +msgid "Unknown Title" +msgstr "Noma'lum sarlavha" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:501 src/ui/media-player.ui:199 +msgid "Unknown Artist" +msgstr "Noma'lum rassom" + +#: src/monitor-manager.c:127 +msgid "Built-in display" +msgstr "O'rnatilgan displey" + +#: src/monitor-manager.c:145 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "%s %s" + +#: src/monitor-manager.c:152 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "%s %s" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:161 +msgid "Unknown" +msgstr "Nomaʼlum" + +#: src/network-auth-prompt.c:202 +#, c-format +msgid "Authentication type of Wi-Fi network “%s” not supported" +msgstr "“%s” Wi-Fi tarmog‘ining autentifikatsiya turi qo‘llab-quvvatlanmaydi" + +#: src/network-auth-prompt.c:207 +#, c-format +msgid "Enter password for the Wi-Fi network “%s”" +msgstr "“%s” Wi-Fi tarmog‘i parolini kiriting" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "Oching" + +#. We don't just set the app_info but name and icon separately to +#. * not overwrite values set by the caller +#: src/notifications/notification.c:422 src/notifications/notification.c:695 +#: src/notifications/notify-manager.c:1013 +msgid "Notification" +msgstr "Bildirishnoma ovozi" + +#. Translators: Point in time, use a short word or abbreviation +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:88 +msgid "now" +msgstr "hozir" + +#. Translators: abbreviated time difference "Less than 30 seconds" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:93 +#, c-format +msgid "<30s" +msgstr "<30s" + +#. Translators: abbreviated time difference "Less than one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:98 +#, c-format +msgid "<1m" +msgstr "<1m" + +#. Translators: abbreviated time difference "About one minute" +#. Please stick to a maximum of 12 chars +#: src/notifications/timestamp-label.c:103 +#, c-format +msgid "~1m" +msgstr "~1m" + +#. Translators: abbreviated, exact time difference "1 minute ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:111 +#, c-format +msgid "%dm" +msgid_plural "%dm" +msgstr[0] "%dm" +msgstr[1] "%dm" + +#. Translators: abbreviated time difference "About 3 hours ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:119 +#, c-format +msgid "~%dh" +msgid_plural "~%dh" +msgstr[0] "~%dsoat" +msgstr[1] "~%dsoat" + +#. Translators: abbreviated time difference "About 1 day ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:124 +#, c-format +msgid "~1d" +msgstr "~1k" + +#. Translators: abbreviated, exact time difference "3 days ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:129 +#, c-format +msgid "%dd" +msgid_plural "%dd" +msgstr[0] "%dd" +msgstr[1] "%dd" + +#. Translators: abbreviated time difference "About 1 month ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:134 +#, c-format +msgid "~1mo" +msgstr "~1 oy" + +#. Translators: abbreviated, exact time difference "3 months ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:139 +#, c-format +msgid "%dmo" +msgid_plural "%dmos" +msgstr[0] "%doy" +msgstr[1] "%dmos" + +#. Translators: abbreviated time difference "About 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:149 +#, c-format +msgid "~%dy" +msgid_plural "~%dy" +msgstr[0] "~%dy" +msgstr[1] "~%dy" + +#. Translators: abbreviated time difference "Over 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:153 +#, c-format +msgid "Over %dy" +msgid_plural "Over %dy" +msgstr[0] "%dy.dan ortiq" +msgstr[1] "%dy.dan ortiq" + +#. Translators: abbreviated time difference "almost 5 years ago" +#. Please stick to a maximum of 4 chars for the time unit +#: src/notifications/timestamp-label.c:158 +#, c-format +msgid "Almost %dy" +msgid_plural "Almost %dy" +msgstr[0] "Deyarli %dy" +msgstr[1] "Deyarli %dy" + +#: src/polkit-auth-agent.c:271 +msgid "Authentication dialog was dismissed by the user" +msgstr "Autentifikatsiya dialogi foydalanuvchi tomonidan oʻchirildi" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:17 +#: src/ui/network-auth-prompt.ui:77 src/ui/polkit-auth-prompt.ui:45 +#: src/ui/system-prompt.ui:28 +msgid "Password:" +msgstr "Parol:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "Kechirasiz, bu ishlamadi. Iltimos, qayta urinib koʻring." + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "" +"Portret nisbatdagi asosiy ekran, bitta taymer 180 daraja aylantirilganini " +"ko’rsatadi" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "Peyzaj" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "O'chiq" + +#: src/rotateinfo.c:126 +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "Yoniq" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "Yopish uchun ESC tugmasini bosing" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "“%s” ishga tushmadi" + +#: src/settings/audio-settings.c:376 +msgid "Phone Shell Volume Control" +msgstr "Phone Shell ovoz balandligini boshqarish" + +#: src/system-prompt.c:376 +msgid "Passwords do not match." +msgstr "Parollar mos kelmaydi." + +#: src/system-prompt.c:383 +msgid "Password cannot be blank" +msgstr "Parol bo'sh bo'lishi mumkin emas" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "Mash'al" + +#: src/ui/app-auth-prompt.ui:43 +msgid "Remember decision" +msgstr "Qarorni eslab qolish" + +#: src/ui/app-auth-prompt.ui:55 src/ui/end-session-dialog.ui:53 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:25 +msgid "Cancel" +msgstr "Bekor qilish" + +#: src/ui/app-grid-button.ui:32 +msgid "Remove from _Favorites" +msgstr "_Sevimlilardan olib tashlash" + +#: src/ui/app-grid-button.ui:37 +msgid "Add to _Favorites" +msgstr "_Sevimlilarga qo'shing" + +#: src/ui/app-grid-button.ui:42 +msgid "View _Details" +msgstr "_Tafsilotlarni ko'rish" + +#: src/ui/app-grid-button.ui:47 +msgid "Uninstall" +msgstr "O'chirish" + +#: src/ui/app-grid-button.ui:54 +msgid "_Remove from Folder" +msgstr "_Jilddan olib tashlash" + +#: src/ui/app-grid.ui:25 +msgid "Search apps…" +msgstr "Ilovalarni qidirish…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "Chiqish qurilmalari yo'q" + +#: src/ui/audio-settings.ui:108 +msgid "Input Devices" +msgstr "Kiritish qurilmalari yo'q" + +#: src/ui/audio-settings.ui:136 +msgid "Sound Settings" +msgstr "Ovoz sozlamalari" + +#: src/ui/bt-status-page.ui:39 +msgid "Enable Bluetooth" +msgstr "Bluetooth-ni yoqing" + +#: src/ui/bt-status-page.ui:54 +msgid "Bluetooth Settings" +msgstr "Bluetooth sozlamalari" + +#: src/ui/emergency-menu.ui:24 +msgid "Close the emergency call dialog" +msgstr "Favqulodda qo'ng'iroqlar oynasini yoping" + +#: src/ui/emergency-menu.ui:50 +msgid "Emergency _Contacts" +msgstr "Favqulodda _Kontaktlar" + +#: src/ui/emergency-menu.ui:57 +msgid "Go to the emergency contacts page" +msgstr "Favqulodda aloqalar sahifasiga o'ting" + +#: src/ui/emergency-menu.ui:80 +msgid "Go back to the emergency dialpad page" +msgstr "Favqulodda terish paneli sahifasiga qayting" + +#: src/ui/emergency-menu.ui:103 +msgid "Owner unknown" +msgstr "Egasi noma'lum" + +#: src/ui/emergency-menu.ui:121 plugins/emergency-info/emergency-info.ui:208 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:170 +msgid "Emergency Contacts" +msgstr "Favqulodda aloqalar" + +#: src/ui/emergency-menu.ui:139 +msgid "No emergency contacts available." +msgstr "Favqulodda aloqalar mavjud emas." + +#: src/ui/end-session-dialog.ui:27 +msgid "Some applications are busy or have unsaved work" +msgstr "Ba'zi ilovalar band yoki saqlanmagan ishlarga ega" + +#: src/ui/gtk-mount-prompt.ui:77 +msgid "User:" +msgstr "Foydalanuvchi:" + +#: src/ui/gtk-mount-prompt.ui:99 +msgid "Domain:" +msgstr "Domen:" + +#: src/ui/gtk-mount-prompt.ui:131 +#, fuzzy +msgid "Co_nnect" +msgstr "Co_nect" + +#: src/ui/lockscreen.ui:42 src/ui/lockscreen.ui:243 +msgid "Back" +msgstr "Orqaga" + +#: src/ui/lockscreen.ui:93 +msgid "Slide up to unlock" +msgstr "Qulfni ochish uchun tepaga suring" + +#: src/ui/lockscreen.ui:330 +msgid "Unlock" +msgstr "Qulfni ochish" + +#: src/ui/network-auth-prompt.ui:6 src/ui/polkit-auth-prompt.ui:5 +msgid "Authentication required" +msgstr "Autentifikatsiya talab qilinadi" + +#: src/ui/network-auth-prompt.ui:37 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:212 +msgid "_Cancel" +msgstr "_Bekor qilish" + +#: src/ui/network-auth-prompt.ui:54 +#, fuzzy +msgid "C_onnect" +msgstr "C_ulanish" + +#: src/ui/polkit-auth-prompt.ui:100 +msgid "Authenticate" +msgstr "Autentifikatsiya" + +#: src/ui/power-menu.ui:106 +msgid "Suspend" +msgstr "To'xtatib turish" + +#: src/ui/power-menu.ui:149 +msgid "Lock" +msgstr "Qulflash" + +#: src/ui/power-menu.ui:225 +msgid "Emergency" +msgstr "Favqulodda holat" + +#: src/ui/run-command-dialog.ui:5 +msgid "Run Command" +msgstr "Buyruqni ishga tushirish" + +#: src/ui/settings.ui:138 +msgid "No notifications" +msgstr "Hech qanday bildirishnoma yo'q" + +#: src/ui/settings.ui:167 +msgid "Notifications" +msgstr "_Bildirishnomalar" + +#: src/ui/settings.ui:176 +msgid "Clear all" +msgstr "Hammasini tozalang" + +#: src/ui/system-prompt.ui:52 +msgid "Confirm:" +msgstr "Tasdiqlash:" + +#: src/ui/top-panel.ui:31 +msgid "_Power Off…" +msgstr "_Oʻchirish…" + +#: src/ui/top-panel.ui:58 +msgid "_Restart…" +msgstr "_Qayta yoqish…" + +#: src/ui/top-panel.ui:85 +msgid "_Suspend…" +msgstr "_Toʻxtatib turish…" + +#: src/ui/top-panel.ui:112 +msgid "_Log Out…" +msgstr "_Chiqish…" + +#: src/ui/wifi-status-page.ui:6 src/wifi-info.c:90 +msgid "Wi-Fi" +msgstr "Wi-fi" + +#: src/ui/wifi-status-page.ui:86 +msgid "Wi-Fi Settings" +msgstr "Wi-Fi sozlamalari" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "VPN" + +#. Translators: This is a time format for a date in +#. long format +#: src/wall-clock.c:239 +msgid "%A, %B %-e" +msgstr "%b %e %H:%M" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "Plagin topilmadi" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "“%s” plaginini yuklab bo‘lmadi." + +#: src/wifi-status-page.c:53 +msgid "No Wi-Fi Device Found" +msgstr "Hech qanday Wi-Fi qurilmasi topilmadi" + +#: src/wifi-status-page.c:57 +msgid "Wi-Fi Disabled" +msgstr "Wi-Fi o'chirilgan" + +#: src/wifi-status-page.c:58 +msgid "Enable Wi-Fi" +msgstr "Wi-Fi-ni yoqing" + +#: src/wifi-status-page.c:61 +msgid "Wi-Fi Hotspot Active" +msgstr "Wi-Fi ulanish nuqtasi faol" + +#: src/wifi-status-page.c:62 +msgid "Turn Off" +msgstr "O'chirib qo'yish" + +#: src/wifi-status-page.c:65 +msgid "No Wi-Fi Hotspots" +msgstr "Wi-Fi ulanish nuqtalari yo'q" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "Uyali" + +#. Translators: Phosh prevents the session from going idle because the caffeine quick setting is toggled +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:53 +msgid "Phosh on caffeine" +msgstr "Kofein haqida Phosh" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-enabled" +msgid "On" +msgstr "Yoniq" + +#: plugins/caffeine-quick-setting/caffeine-quick-setting.c:132 +msgctxt "caffeine-disabled" +msgid "Off" +msgstr "O'chirilgan" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:36 +msgid "Default style" +msgstr "Dastlabki uslub" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:37 +msgid "Dark mode" +msgstr "Qorong'i rejim" + +#: plugins/dark-mode-quick-setting/dark-mode-quick-setting.c:38 +msgid "Light mode" +msgstr "Yorug’ rejimi" + +#: plugins/emergency-info/emergency-info.ui:40 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:62 +msgid "Personal Information" +msgstr "Shaxsiy ma'lumot" + +#: plugins/emergency-info/emergency-info.ui:48 +msgid "Date of Birth" +msgstr "Tug'ilgan kuni" + +#: plugins/emergency-info/emergency-info.ui:68 +msgid "Preferred Language" +msgstr "Afzal til" + +#: plugins/emergency-info/emergency-info.ui:88 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:85 +msgid "Home Address" +msgstr "Uy manzili" + +#: plugins/emergency-info/emergency-info.ui:96 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Medical Information" +msgstr "Tibbiy ma'lumot" + +#: plugins/emergency-info/emergency-info.ui:104 +msgid "Age" +msgstr "Yosh" + +#: plugins/emergency-info/emergency-info.ui:124 +msgid "Blood Type" +msgstr "Qon turi" + +#: plugins/emergency-info/emergency-info.ui:144 +msgid "Height" +msgstr "Balandligi" + +#: plugins/emergency-info/emergency-info.ui:164 +msgid "Weight" +msgstr "Og'irlik" + +#: plugins/emergency-info/emergency-info.ui:184 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Allergies" +msgstr "Allergiya" + +#: plugins/emergency-info/emergency-info.ui:192 +msgid "Medications & Conditions" +msgstr "Dorilar va shartlar" + +#: plugins/emergency-info/emergency-info.ui:200 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:155 +msgid "Other Information" +msgstr "Boshqa ma'lumotlar" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:16 +msgid "Emergency Info Preferences" +msgstr "Favqulodda ma'lumot sozlamalari" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:36 +msgid "Done" +msgstr "Bajarildi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:67 +msgid "_Owner Name" +msgstr "_Egasining ismi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:73 +msgid "_Date of Birth" +msgstr "_Tug'ilgan kuni" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:79 +msgid "_Preferred Language" +msgstr "_Afzal til" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:105 +msgid "_Age" +msgstr "Yosh reytingi haqida ma'lumot yo'q" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:111 +msgid "_Blood Type" +msgstr "_Qon turi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:117 +msgid "_Height" +msgstr "Chiziq balandligi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:123 +msgid "_Weight" +msgstr "_Og'irlik" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:142 +msgid "Medications and Conditions" +msgstr "Dori-darmonlar va shartlar" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:179 +msgid "Add Contact" +msgstr "_Kontakt qo'shish" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:203 +msgid "Add New Contact" +msgstr "Yangi kontakt qo'shish" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:220 +msgid "_Add" +msgstr "_Qo’shish" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:240 +msgid "_Contact Name" +msgstr "_Kontakt nomi" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:246 +msgid "Relationship" +msgstr "Aloqa" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:252 +msgid "_Contact Number" +msgstr "_Aloqa raqami" + +#: plugins/launcher-box/launcher-box.ui:15 +msgid "No launchers configured" +msgstr "Hech qanday ishga tushirgich sozlanmagan" + +#: plugins/launcher-box/launcher-box.ui:30 +msgid "Launchers" +msgstr "Ishga tushiruvchilar" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data On" +msgstr "_Mobil ma'lumotlar" + +#: plugins/mobile-data-quick-setting/mobile-data-quick-setting.c:80 +msgid "Mobile Data Off" +msgstr "Mobil maʼlumot oʻchirilgan" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light On" +msgstr "Tungi chiroq yoniq" + +#: plugins/night-light-quick-setting/night-light-quick-setting.c:65 +msgid "Night Light Off" +msgstr "Tungi chiroq oʻchirilgan" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:71 +msgid "Pomodoro start" +msgstr "Pomodoro boshlanishi" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:72 +#, c-format +msgid "Focus on your task for %d minutes" +msgstr "%d daqiqa davomida diqqatingizni vazifangizga qarating" + +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:77 +msgid "Take a break" +msgstr "Tanaffus qiling" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:79 +#, c-format +msgid "You have %d minutes until next Pomodoro" +msgstr "Keyingi Pomodorogacha %d daqiqa bor" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:94 +msgid "Pomodoro Timer" +msgstr "Pomodoro taymer" + +#. Translators: Pomodoro is a technique, no need to translate it +#: plugins/pomodoro-quick-setting/pomodoro-quick-setting.c:117 +#, c-format +msgid "Pomodoro Off" +msgstr "Pomodoro oʻchirilgan" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:6 +msgid "Pomodoro Quick Setting Preferences" +msgstr "Pomodoro tezkor sozlamalari" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:11 +msgid "Pomodoro Technique" +msgstr "Pomodoro texnikasi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:14 +msgid "_Active Duration" +msgstr "_Faol davomiyligi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:15 +msgid "Duration of the focus session" +msgstr "Fokus sessiyasining davomiyligi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:29 +msgid "_Break Duration" +msgstr "_Tanaffus davomiyligi" + +#: plugins/pomodoro-quick-setting/prefs/prefs.ui:30 +msgid "Duration of the break between sessions" +msgstr "Mashg'ulotlar orasidagi tanaffusning davomiyligi" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "Ko'rsatiladigan hujjat yo'q" + +#: plugins/ticket-box/ticket-box.ui:78 +msgid "Tickets" +msgstr "Chiptalar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:94 +msgid "_Open" +msgstr "Oching" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:95 +msgid "Choose Folder" +msgstr "Jildni tanlang" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "Chipta qutisi afzalliklari" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:10 +msgid "Paths" +msgstr "Yo'llar" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:14 +msgid "Folder Settings" +msgstr "Papka sozlamalari" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:15 +msgid "Where Phosh looks for your tickets" +msgstr "Phosh chiptalaringizni qaerdan qidiradi" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Ticket Folder" +msgstr "Chipta papkasi" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "Bugun" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "Ertaga" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %u day" +msgid_plural "In %u days" +msgstr[0] "%u kundan keyin" +msgstr[1] "%u kundan keyin" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "Tadbir yo’q" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "%R" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "%l:%M %p" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "Kun bo'yi" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "Tugaydi" + +#: plugins/upcoming-events/upcoming-event.c:400 +msgid "Untitled event" +msgstr "Nomsiz hodisa" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:6 +msgid "Upcoming Events Preferences" +msgstr "Kelgusi tadbirlar sozlamalari" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:10 +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:17 +msgid "Days" +msgstr "Kun" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:13 +msgid "Date Range" +msgstr "Sana oralig'i" + +#: plugins/upcoming-events/prefs/upcoming-events-prefs.ui:14 +msgid "Number of days to show events for" +msgstr "Tadbirlarni ko'rsatish uchun kunlar soni" + +#: plugins/scaling-quick-setting/qs.ui:14 +msgid "Monitor scales" +msgstr "Monitor o'lchovlari" + +#. Translators: This is scale factor of a monitor in percent +#: plugins/scaling-quick-setting/scale-row.c:48 +#: plugins/scaling-quick-setting/scaling-quick-setting.c:93 +#, c-format +msgid "%d%%" +msgstr "%d%%" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:79 +msgid "Hotspot On" +msgstr "%s Hotspot" + +#: plugins/wifi-hotspot-quick-setting/wifi-hotspot-quick-setting.c:81 +msgid "Hotspot Off" +msgstr "Hotspot oʻchirilgan" diff --git a/po/zh_CN.po b/po/zh_CN.po new file mode 100644 index 000000000..2e369d4af --- /dev/null +++ b/po/zh_CN.po @@ -0,0 +1,797 @@ +# Chinese (China) translation for phosh. +# Copyright (C) 2020 phosh's COPYRIGHT HOLDER +# This file is distributed under the same license as the phosh package. +# Damned , 2020. +# +msgid "" +msgstr "" +"Project-Id-Version: phosh master\n" +"Report-Msgid-Bugs-To: https://gitlab.gnome.org/World/Phosh/phosh/issues\n" +"POT-Creation-Date: 2023-07-31 20:22+0000\n" +"PO-Revision-Date: 2023-08-09 15:24-0400\n" +"Last-Translator: \n" +"Language-Team: Chinese (China) \n" +"Language: zh_CN\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.4.3\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 data/wayland-sessions/phosh.desktop:3 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 data/wayland-sessions/phosh.desktop:4 +msgid "Phone Shell" +msgstr "手机 Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "用于移动设备的窗口管理和应用程序启动" + +#: data/wayland-sessions/phosh.desktop:5 +msgid "This session logs you into Phosh" +msgstr "" + +#: plugins/calendar/calendar.desktop.in.in:5 +msgid "Calendar" +msgstr "日历" + +#: plugins/calendar/calendar.desktop.in.in:6 +msgid "A simple calendar widget" +msgstr "" + +#: plugins/ticket-box/ticket-box.desktop.in.in:4 +#: plugins/ticket-box/ticket-box.ui:14 +msgid "Ticket Box" +msgstr "" + +#: plugins/ticket-box/ticket-box.desktop.in.in:5 +msgid "Show PDFs on the lock screen. This plugin is experimental." +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:4 +msgid "Upcoming Events" +msgstr "" + +#: plugins/upcoming-events/upcoming-events.desktop.in.in:5 +msgid "Show upcoming calendar events" +msgstr "" + +#: src/app-grid-button.c:529 +msgid "Application" +msgstr "应用程序" + +#: src/app-grid.c:137 +msgid "Show All Apps" +msgstr "显示全部应用" + +#: src/app-grid.c:140 +msgid "Show Only Mobile Friendly Apps" +msgstr "" + +#: src/bt-info.c:92 +msgctxt "bluetooth:enabled" +msgid "On" +msgstr "开启" + +#: src/bt-info.c:94 +msgid "Bluetooth" +msgstr "蓝牙" + +#: src/call-notification.c:61 +msgid "Unknown caller" +msgstr "未知来电者" + +#: src/docked-info.c:81 +msgid "Docked" +msgstr "" + +#: src/docked-info.c:81 src/docked-info.c:199 +msgid "Undocked" +msgstr "" + +#: src/emergency-menu.c:97 src/ui/app-auth-prompt.ui:71 +#: src/ui/end-session-dialog.ui:71 +msgid "Ok" +msgstr "" + +#: src/emergency-menu.c:100 +msgid "Unable to place emergency call" +msgstr "" + +#: src/emergency-menu.c:105 +msgid "Internal error" +msgstr "" + +#: src/end-session-dialog.c:163 +msgid "Log Out" +msgstr "" + +#: src/end-session-dialog.c:166 +#, c-format +msgid "%s will be logged out automatically in %d second." +msgid_plural "%s will be logged out automatically in %d seconds." +msgstr[0] "" + +#: src/end-session-dialog.c:172 +msgid "Power Off" +msgstr "关机" + +#: src/end-session-dialog.c:173 +#, c-format +msgid "The system will power off automatically in %d second." +msgid_plural "The system will power off automatically in %d seconds." +msgstr[0] "" + +#: src/end-session-dialog.c:179 +msgid "Restart" +msgstr "重启" + +#: src/end-session-dialog.c:180 +#, c-format +msgid "The system will restart automatically in %d second." +msgid_plural "The system will restart automatically in %d seconds." +msgstr[0] "" + +#: src/end-session-dialog.c:270 +msgid "Unknown application" +msgstr "未知应用程序" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:97 +msgid "Quiet" +msgstr "振动" + +#. Translators: quiet and silent are fbd profiles names: +#. see https://source.puri.sm/Librem5/feedbackd#profiles +#. for details +#: src/feedbackinfo.c:104 +msgid "Silent" +msgstr "静音" + +#. Translators: Enable LED, haptic and audio feedback +#: src/feedbackinfo.c:108 +msgctxt "feedback:enabled" +msgid "On" +msgstr "开启" + +#: src/location-manager.c:268 +#, c-format +msgid "Allow '%s' to access your location information?" +msgstr "" + +#: src/location-manager.c:273 +msgid "Geolocation" +msgstr "地理位置" + +#: src/location-manager.c:274 +msgid "Yes" +msgstr "是" + +#: src/location-manager.c:274 +msgid "No" +msgstr "否" + +#: src/lockscreen.c:174 src/ui/lockscreen.ui:245 +msgid "Enter Passcode" +msgstr "输入密码" + +#: src/lockscreen.c:397 +msgid "Checking…" +msgstr "正在检查…" + +#: src/screenshot-manager.c:213 +msgid "Screenshot" +msgstr "" + +#: src/screenshot-manager.c:214 +msgid "Screenshot copied to clipboard" +msgstr "" + +#. Translators: Used when the title of a song is unknown +#: src/media-player.c:321 src/ui/media-player.ui:161 +msgid "Unknown Title" +msgstr "未知标题" + +#. Translators: Used when the artist of a song is unknown +#: src/media-player.c:329 src/ui/media-player.ui:148 +msgid "Unknown Artist" +msgstr "未知艺术家" + +#: src/monitor-manager.c:119 +msgid "Built-in display" +msgstr "内置显示器" + +#: src/monitor-manager.c:137 +#, c-format +msgctxt "" +"This is a monitor vendor name, followed by a size in inches, like 'Dell 15\"'" +msgid "%s %s" +msgstr "" + +#: src/monitor-manager.c:144 +#, c-format +msgctxt "" +"This is a monitor vendor name followed by product/model name where size in " +"inches could not be calculated, e.g. Dell U2414H" +msgid "%s %s" +msgstr "" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:153 +msgid "Unknown" +msgstr "未知" + +#: src/network-auth-prompt.c:201 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "Wifi 网络“%s” 的认证类型不受支持" + +#: src/network-auth-prompt.c:206 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "输入 wifi 网络“%s”的密码" + +#: src/notifications/mount-notification.c:121 +msgid "Open" +msgstr "" + +#: src/notifications/notification.c:383 src/notifications/notification.c:654 +msgid "Notification" +msgstr "通知" + +#. Translators: Timestamp seconds suffix +#: src/notifications/timestamp-label.c:84 +msgctxt "timestamp-suffix-seconds" +msgid "s" +msgstr "秒" + +#. Translators: Timestamp minute suffix +#: src/notifications/timestamp-label.c:86 +msgctxt "timestamp-suffix-minute" +msgid "m" +msgstr "分" + +#. Translators: Timestamp minutes suffix +#: src/notifications/timestamp-label.c:88 +msgctxt "timestamp-suffix-minutes" +msgid "m" +msgstr "分" + +#. Translators: Timestamp hour suffix +#: src/notifications/timestamp-label.c:90 +msgctxt "timestamp-suffix-hour" +msgid "h" +msgstr "时" + +#. Translators: Timestamp hours suffix +#: src/notifications/timestamp-label.c:92 +msgctxt "timestamp-suffix-hours" +msgid "h" +msgstr "时" + +#. Translators: Timestamp day suffix +#: src/notifications/timestamp-label.c:94 +msgctxt "timestamp-suffix-day" +msgid "d" +msgstr "日" + +#. Translators: Timestamp days suffix +#: src/notifications/timestamp-label.c:96 +msgctxt "timestamp-suffix-days" +msgid "d" +msgstr "日" + +#. Translators: Timestamp month suffix +#: src/notifications/timestamp-label.c:98 +msgctxt "timestamp-suffix-month" +msgid "mo" +msgstr "月" + +#. Translators: Timestamp months suffix +#: src/notifications/timestamp-label.c:100 +msgctxt "timestamp-suffix-months" +msgid "mos" +msgstr "月" + +#. Translators: Timestamp year suffix +#: src/notifications/timestamp-label.c:102 +msgctxt "timestamp-suffix-year" +msgid "y" +msgstr "年" + +#. Translators: Timestamp years suffix +#: src/notifications/timestamp-label.c:104 +msgctxt "timestamp-suffix-years" +msgid "y" +msgstr "年" + +#: src/notifications/timestamp-label.c:121 +#, fuzzy +#| msgid "Unknown" +msgid "now" +msgstr "未知" + +#. Translators: time difference "Over 5 years" +#: src/notifications/timestamp-label.c:189 +#, fuzzy, c-format +#| msgid "Over" +msgid "Over %dy" +msgstr "超过" + +#. Translators: time difference "almost 5 years" +#: src/notifications/timestamp-label.c:193 +#, fuzzy, c-format +#| msgid "Almost" +msgid "Almost %dy" +msgstr "将近" + +#. Translators: a time difference like '<5m', if in doubt leave untranslated +#: src/notifications/timestamp-label.c:200 +#, c-format +msgid "%s%d%s" +msgstr "" + +#: src/polkit-auth-agent.c:227 +msgid "Authentication dialog was dismissed by the user" +msgstr "认证对话框已被用户关闭" + +#: src/polkit-auth-prompt.c:278 src/ui/gtk-mount-prompt.ui:20 +#: src/ui/network-auth-prompt.ui:82 src/ui/polkit-auth-prompt.ui:56 +#: src/ui/system-prompt.ui:32 +msgid "Password:" +msgstr "密码:" + +#: src/polkit-auth-prompt.c:325 +msgid "Sorry, that didn’t work. Please try again." +msgstr "抱歉,密码无效。请重试。" + +#: src/rotateinfo.c:102 +msgid "Portrait" +msgstr "纵向" + +#: src/rotateinfo.c:105 +msgid "Landscape" +msgstr "横向" + +#: src/rotateinfo.c:125 src/rotateinfo.c:230 +msgctxt "automatic-screen-rotation-disabled" +msgid "Off" +msgstr "" + +#: src/rotateinfo.c:126 +#, fuzzy +#| msgid "On" +msgctxt "automatic-screen-rotation-enabled" +msgid "On" +msgstr "开启" + +#: src/run-command-dialog.c:129 +msgid "Press ESC to close" +msgstr "" + +#: src/run-command-manager.c:89 +#, c-format +msgid "Running '%s' failed" +msgstr "" + +#: src/settings/audio-settings.c:373 +msgid "Phone Shell Volume Control" +msgstr "" + +#: src/system-prompt.c:365 +msgid "Passwords do not match." +msgstr "密码不匹配。" + +#: src/system-prompt.c:372 +msgid "Password cannot be blank" +msgstr "密码不能为空" + +#: src/torch-info.c:80 +msgid "Torch" +msgstr "" + +#: src/ui/app-auth-prompt.ui:49 +msgid "Remember decision" +msgstr "" + +#: src/ui/app-auth-prompt.ui:62 src/ui/end-session-dialog.ui:62 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:29 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:289 +#, fuzzy +#| msgid "_Cancel" +msgid "Cancel" +msgstr "取消(_C)" + +#: src/ui/app-grid-button.ui:55 +msgid "App" +msgstr "应用程序" + +#: src/ui/app-grid-button.ui:79 +msgid "Remove from _Favorites" +msgstr "从收藏夹中移除(_F)" + +#: src/ui/app-grid-button.ui:84 +msgid "Add to _Favorites" +msgstr "添加到收藏夹(_F)" + +#: src/ui/app-grid-button.ui:89 +msgid "View _Details" +msgstr "" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "搜索应用程序…" + +#: src/ui/audio-settings.ui:84 +msgid "Output Devices" +msgstr "" + +#: src/ui/audio-settings.ui:107 +msgid "Input Devices" +msgstr "" + +#: src/ui/audio-settings.ui:134 +msgid "Sound Settings" +msgstr "" + +#: src/ui/emergency-menu.ui:26 +msgid "Close the emergency call dialog" +msgstr "" + +#: src/ui/emergency-menu.ui:52 +msgid "Emergency _Contacts" +msgstr "紧急联系人(_C)" + +#: src/ui/emergency-menu.ui:59 +msgid "Go to the emergency contacts page" +msgstr "" + +#: src/ui/emergency-menu.ui:83 +msgid "Go back to the emergency dialpad page" +msgstr "" + +#: src/ui/emergency-menu.ui:106 +#, fuzzy +#| msgid "Unknown" +msgid "Owner unknown" +msgstr "未知" + +#: src/ui/emergency-menu.ui:124 plugins/emergency-info/emergency-info.ui:195 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:230 +#, fuzzy +#| msgid "Emergency" +msgid "Emergency Contacts" +msgstr "紧急呼叫" + +#: src/ui/emergency-menu.ui:142 +msgid "No emergency contacts available." +msgstr "" + +#: src/ui/end-session-dialog.ui:31 +msgid "Some applications are busy or have unsaved work" +msgstr "" + +#: src/ui/gtk-mount-prompt.ui:94 +msgid "User:" +msgstr "用户:" + +#: src/ui/gtk-mount-prompt.ui:117 +msgid "Domain:" +msgstr "" + +#: src/ui/gtk-mount-prompt.ui:150 +msgid "Co_nnect" +msgstr "连接(_N)" + +#: src/ui/lockscreen.ui:36 src/ui/lockscreen.ui:334 +msgid "Back" +msgstr "" + +#: src/ui/lockscreen.ui:97 +msgid "Slide up to unlock" +msgstr "向上滑动以解锁" + +#: src/ui/lockscreen.ui:297 +msgid "Unlock" +msgstr "解锁" + +#: src/ui/network-auth-prompt.ui:5 src/ui/polkit-auth-prompt.ui:6 +#, fuzzy +#| msgid "Authenticate" +msgid "Authentication required" +msgstr "认证" + +#: src/ui/network-auth-prompt.ui:40 +#: plugins/ticket-box/prefs/ticket-box-prefs.c:90 +msgid "_Cancel" +msgstr "取消(_C)" + +#: src/ui/network-auth-prompt.ui:58 +msgid "C_onnect" +msgstr "连接(_O)" + +#: src/ui/polkit-auth-prompt.ui:122 +msgid "Authenticate" +msgstr "认证" + +#: src/ui/power-menu.ui:69 +#, fuzzy +#| msgid "Power Off" +msgid "_Power Off" +msgstr "关机" + +#: src/ui/power-menu.ui:110 +msgid "_Lock" +msgstr "" + +#: src/ui/power-menu.ui:151 +msgid "_Screenshot" +msgstr "" + +#: src/ui/power-menu.ui:192 +#, fuzzy +#| msgid "Emergency" +msgid "_Emergency" +msgstr "紧急呼叫" + +#: src/ui/run-command-dialog.ui:6 +msgid "Run Command" +msgstr "" + +#: src/ui/settings.ui:296 +#, fuzzy +#| msgid "Notification" +msgid "No notifications" +msgstr "通知" + +#: src/ui/settings.ui:336 +msgid "Clear all" +msgstr "" + +#: src/ui/system-prompt.ui:62 +msgid "Confirm:" +msgstr "确认:" + +#: src/ui/top-panel.ui:32 +#, fuzzy +#| msgid "Power Off" +msgid "_Power Off…" +msgstr "关机" + +#: src/ui/top-panel.ui:60 +#, fuzzy +#| msgid "Restart" +msgid "_Restart…" +msgstr "重启" + +#: src/ui/top-panel.ui:88 +msgid "_Suspend…" +msgstr "" + +#: src/ui/top-panel.ui:116 +msgid "_Log Out…" +msgstr "" + +#. Translators: This is a time format for a date in +#. long format +#: src/util.c:339 +#, fuzzy +#| msgid "%A, %B %d" +msgid "%A, %B %-e" +msgstr "%m 月 %d 日,%A" + +#: src/vpn-info.c:89 +msgid "VPN" +msgstr "" + +#: src/widget-box.c:54 +msgid "Plugin not found" +msgstr "" + +#: src/widget-box.c:57 +#, c-format +msgid "The plugin '%s' could not be loaded." +msgstr "" + +#: src/wifiinfo.c:90 +msgid "Wi-Fi" +msgstr "Wi-Fi" + +#. Translators: Refers to the cellular wireless network +#: src/wwan-info.c:210 +msgid "Cellular" +msgstr "蜂窝网络" + +#: plugins/emergency-info/emergency-info.ui:39 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:70 +msgid "Personal Information" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:47 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:88 +msgid "Date of Birth" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:65 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:100 +msgid "Preferred Language" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:83 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:112 +msgid "Home Address" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:91 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:129 +msgid "Medical Information" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:99 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:135 +msgid "Age" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:117 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:147 +msgid "Blood Type" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:135 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:159 +msgid "Height" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:153 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:171 +msgid "Weight" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:171 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:183 +msgid "Allergies" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:179 +msgid "Medications & Conditions" +msgstr "" + +#: plugins/emergency-info/emergency-info.ui:187 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:213 +msgid "Other Information" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:6 +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:19 +msgid "Emergency Info Preferences" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:40 +msgid "Done" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:76 +msgid "Owner Name" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:198 +msgid "Medications and Conditions" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:239 +msgid "Add Contact" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:284 +msgid "Add New Contact" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:294 +msgid "Add" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:315 +msgid "New Contact Name" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:329 +msgid "Relationship" +msgstr "" + +#: plugins/emergency-info/prefs/emergency-info-prefs.ui:342 +msgid "Number" +msgstr "" + +#: plugins/ticket-box/ticket-box.ui:15 +msgid "No documents to display" +msgstr "" + +#: plugins/ticket-box/ticket-box.ui:83 +msgid "Tickets" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:86 +msgid "Choose Folder" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.c:89 +msgid "_Open" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:6 +msgid "Ticket Box Preferences" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:13 +msgid "Paths" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:18 +msgid "Folder Settings" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:19 +msgid "Where Phosh looks for your tickets" +msgstr "" + +#: plugins/ticket-box/prefs/ticket-box-prefs.ui:22 +msgid "Ticket Folder" +msgstr "" + +#: plugins/upcoming-events/event-list.c:142 +msgid "Today" +msgstr "" + +#: plugins/upcoming-events/event-list.c:144 +msgid "Tomorrow" +msgstr "" + +#: plugins/upcoming-events/event-list.c:150 +#, c-format +msgid "In %d day" +msgid_plural "In %d days" +msgstr[0] "" + +#: plugins/upcoming-events/event-list.ui:26 +msgid "No events" +msgstr "" + +#. Translators: This is the time format used in 24-hour mode. +#: plugins/upcoming-events/upcoming-event.c:56 +msgid "%R" +msgstr "" + +#. Translators: This is the time format used in 12-hour mode. +#: plugins/upcoming-events/upcoming-event.c:59 +msgid "%l:%M %p" +msgstr "" + +#. Translators: An all day event +#: plugins/upcoming-events/upcoming-event.c:122 +#: plugins/upcoming-events/upcoming-event.c:159 +msgid "All day" +msgstr "" + +#. Translators: When the event ends: Ends\r16:00 +#: plugins/upcoming-events/upcoming-event.c:148 +msgid "Ends" +msgstr "" + +#: plugins/upcoming-events/upcoming-event.c:398 +msgid "Untitled event" +msgstr "" + +#~ msgid "%d.%m.%y" +#~ msgstr "%y.%m.%d" + +#~ msgid "Lock Screen" +#~ msgstr "锁定屏幕" + +#~ msgid "Logout" +#~ msgstr "注销" diff --git a/po/zh_Hans_CN.po b/po/zh_Hans_CN.po new file mode 100644 index 000000000..d7e2023c3 --- /dev/null +++ b/po/zh_Hans_CN.po @@ -0,0 +1,145 @@ +# Lei Chen , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-03-14 13:18+0100\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"PO-Revision-Date: 2019-03-19 11:27+0000\n" +"Last-Translator: Lei Chen \n" +"Language-Team: Chinese (Simplified, China)\n" +"Language: zh_Hans_CN\n" +"X-Generator: Zanata 4.6.2\n" +"Plural-Forms: nplurals=1; plural=0\n" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "手机Shell" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "窗口管理和应用启动" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: src/app-grid-button.c:523 +msgid "Application" +msgstr "" + +#: src/feedbackinfo.c:38 +msgid "Quiet" +msgstr "" + +#: src/feedbackinfo.c:40 +msgid "Silent" +msgstr "" + +#: src/feedbackinfo.c:42 +msgid "On" +msgstr "" + +#: src/lockscreen.c:78 src/ui/lockscreen.ui:204 +msgid "Enter Passcode" +msgstr "" + +#: src/lockscreen.c:257 +msgid "Checking…" +msgstr "" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:334 +msgid "%A, %B %-e" +msgstr "" + +#: src/monitor-manager.c:53 +msgid "Built-in display" +msgstr "内置显示器" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:57 +msgid "Unknown" +msgstr "未知" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "" + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "" + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "密码不匹配。" + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "密码不能为空" + +#: src/wifiinfo.c:55 +msgid "Wi-Fi" +msgstr "" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "" + +#: src/ui/lockscreen.ui:36 +msgid "Slide up to unlock" +msgstr "向上滑以解锁" + +#: src/ui/lockscreen.ui:250 +msgid "Emergency" +msgstr "" + +#: src/ui/lockscreen.ui:266 +msgid "Unlock" +msgstr "" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "" diff --git a/po/zh_TW.po b/po/zh_TW.po new file mode 100644 index 000000000..057b2f753 --- /dev/null +++ b/po/zh_TW.po @@ -0,0 +1,171 @@ +# Martin Chang , 2018. #zanata +# Martin Chang , 2019. #zanata +msgid "" +msgstr "" +"Project-Id-Version: phosh\n" +"Report-Msgid-Bugs-To: https://source.puri.sm/Librem5/phosh/issues\n" +"POT-Creation-Date: 2020-04-23 03:36+0000\n" +"PO-Revision-Date: 2020-05-17 14:18+0800\n" +"Last-Translator: Yi-Jyun Pan \n" +"Language-Team: Chinese (Taiwan)\n" +"Language: zh_TW\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 2.3.1\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#. Translators: this is the session name, no need to translate it +#: data/phosh.session.desktop.in.in:4 +msgid "Phosh" +msgstr "Phosh" + +#: data/mobi.phosh.Shell.desktop.in.in:4 +msgid "Phone Shell" +msgstr "手機殼層" + +#: data/mobi.phosh.Shell.desktop.in.in:5 +msgid "Window management and application launching for mobile" +msgstr "視窗管理與手機的應用程式啟動器" + +#: src/app-grid-button.c:527 +msgid "Application" +msgstr "應用程式" + +#: src/feedbackinfo.c:38 +msgid "Quiet" +msgstr "安靜" + +#: src/feedbackinfo.c:40 +msgid "Silent" +msgstr "寂靜" + +#: src/feedbackinfo.c:42 +msgid "On" +msgstr "開啟" + +#: src/lockscreen.c:74 src/ui/lockscreen.ui:204 +msgid "Enter Passcode" +msgstr "請輸入密碼" + +#: src/lockscreen.c:253 +msgid "Checking…" +msgstr "檢查中…" + +#. Translators: This is a time format for a date in +#. long format +#: src/lockscreen.c:330 +msgid "%A, %B %-e" +msgstr "%A, %B %-e" + +#: src/monitor-manager.c:53 +msgid "Built-in display" +msgstr "內建顯示器" + +#. Translators: An unknown monitor type +#: src/monitor-manager.c:57 +msgid "Unknown" +msgstr "未知" + +#: src/network-auth-prompt.c:184 +#, c-format +msgid "Authentication type of wifi network “%s” not supported" +msgstr "不支援「%s」Wi-Fi 網路的認證類型" + +#: src/network-auth-prompt.c:189 +#, c-format +msgid "Enter password for the wifi network “%s”" +msgstr "請輸入「%s」Wi-Fi 網路的密碼" + +#: src/notifications/notification.c:298 src/notifications/notification.c:504 +msgid "Notification" +msgstr "通知" + +#: src/polkit-auth-agent.c:229 +msgid "Authentication dialog was dismissed by the user" +msgstr "認證對話框被使用者關閉" + +#: src/polkit-auth-prompt.c:276 src/ui/network-auth-prompt.ui:128 +#: src/ui/polkit-auth-prompt.ui:41 src/ui/system-prompt.ui:39 +msgid "Password:" +msgstr "密碼:" + +#: src/polkit-auth-prompt.c:322 +msgid "Sorry, that didn’t work. Please try again." +msgstr "抱歉,那沒有用。請再試一次。" + +#: src/polkit-auth-prompt.c:488 +msgid "Authenticate" +msgstr "認證" + +#: src/system-prompt.c:371 +msgid "Passwords do not match." +msgstr "密碼不相符。" + +#: src/system-prompt.c:378 +msgid "Password cannot be blank" +msgstr "密碼不可空白" + +#: src/ui/app-grid-button.ui:48 +msgid "App" +msgstr "App" + +#: src/ui/app-grid-button.ui:75 +msgid "Remove from _Favorites" +msgstr "從收藏中移除(_F)" + +#: src/ui/app-grid-button.ui:80 +msgid "Add to _Favorites" +msgstr "加入收藏(_F)" + +#: src/ui/app-grid.ui:21 +msgid "Search apps…" +msgstr "尋找應用程式…" + +#: src/ui/lockscreen.ui:36 +msgid "Slide up to unlock" +msgstr "向上滑動以解鎖" + +#: src/ui/lockscreen.ui:250 +msgid "Emergency" +msgstr "緊急模式" + +#: src/ui/lockscreen.ui:266 +msgid "Unlock" +msgstr "解鎖" + +#: src/ui/network-auth-prompt.ui:90 +msgid "_Cancel" +msgstr "取消(_C)" + +#: src/ui/network-auth-prompt.ui:106 +msgid "C_onnect" +msgstr "連線(_O)" + +#: src/ui/polkit-auth-prompt.ui:105 +msgid "User:" +msgstr "使用者:" + +#: src/ui/system-prompt.ui:69 +msgid "Confirm:" +msgstr "確認:" + +#: src/ui/top-panel.ui:15 +msgid "Lock Screen" +msgstr "鎖定螢幕" + +#: src/ui/top-panel.ui:22 +msgid "Suspend" +msgstr "暫停" + +#: src/ui/top-panel.ui:29 +msgid "Logout" +msgstr "登出" + +#: src/ui/top-panel.ui:36 +msgid "Power Off" +msgstr "關機" + +#: src/wifiinfo.c:55 +msgid "Wi-Fi" +msgstr "Wi-Fi" diff --git a/protocol/input-method-unstable-v2.xml b/protocol/input-method-unstable-v2.xml new file mode 100644 index 000000000..62be9d946 --- /dev/null +++ b/protocol/input-method-unstable-v2.xml @@ -0,0 +1,490 @@ + + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2011 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2012, 2013 Intel Corporation + Copyright © 2015, 2016 Jan Arne Petersen + Copyright © 2017, 2018 Red Hat, Inc. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows applications to act as input methods for compositors. + + An input method context is used to manage the state of the input method. + + Text strings are UTF-8 encoded, their indices and lengths are in bytes. + + This document adheres to the RFC 2119 when using words like "must", + "should", "may", etc. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + An input method object allows for clients to compose text. + + The objects connects the client to a text input in an application, and + lets the client to serve as an input method for a seat. + + The zwp_input_method_v2 object can occupy two distinct states: active and + inactive. In the active state, the object is associated to and + communicates with a text input. In the inactive state, there is no + associated text input, and the only communication is with the compositor. + Initially, the input method is in the inactive state. + + Requests issued in the inactive state must be accepted by the compositor. + Because of the serial mechanism, and the state reset on activate event, + they will not have any effect on the state of the next text input. + + There must be no more than one input method object per seat. + + + + + Notification that a text input focused on this seat requested the input + method to be activated. + + This event serves the purpose of providing the compositor with an + active input method. + + This event resets all state associated with previous enable, disable, + surrounding_text, text_change_cause, and content_type events, as well + as the state associated with set_preedit_string, commit_string, and + delete_surrounding_text requests. In addition, it marks the + zwp_input_method_v2 object as active, and makes any existing + zwp_input_popup_surface_v2 objects visible. + + The surrounding_text, and content_type events must follow before the + next done event if the text input supports the respective + functionality. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Notification that no focused text input currently needs an active + input method on this seat. + + This event marks the zwp_input_method_v2 object as inactive. The + compositor must make all existing zwp_input_popup_surface_v2 objects + invisible until the next activate event. + + State set with this event is double-buffered. It will get applied on + the next zwp_input_method_v2.done event, and stay valid until changed. + + + + + + Updates the surrounding plain text around the cursor, excluding the + preedit text. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. + + The argument text is a buffer containing the preedit string, and must + include the cursor position, and the complete selection. It should + contain additional characters before and after these. There is a + maximum length of wayland messages, so text can not be longer than 4000 + bytes. + + cursor is the byte offset of the cursor within the text buffer. + + anchor is the byte offset of the selection anchor within the text + buffer. If there is no selected text, anchor must be the same as + cursor. + + If this event does not arrive before the first done event, the input + method may assume that the text input does not support this + functionality and ignore following surrounding_text events. + + Values set with this event are double-buffered. They will get applied + and set to initial values on the next zwp_input_method_v2.done + event. + + The initial state for affected fields is empty, meaning that the text + input does not support sending surrounding text. If the empty values + get applied, subsequent attempts to change them may have no effect. + + + + + + + + + Tells the input method why the text surrounding the cursor changed. + + Whenever the client detects an external change in text, cursor, or + anchor position, it must issue this request to the compositor. This + request is intended to give the input method a chance to update the + preedit text in an appropriate way, e.g. by removing it when the user + starts typing with a keyboard. + + cause describes the source of the change. + + The value set with this event is double-buffered. It will get applied + and set to its initial value on the next zwp_input_method_v2.done + event. + + The initial value of cause is input_method. + + + + + + + Indicates the content type and hint for the current + zwp_input_method_v2 instance. + + Values set with this event are double-buffered. They will get applied + on the next zwp_input_method_v2.done event. + + The initial value for hint is none, and the initial value for purpose + is normal. + + + + + + + + Atomically applies state changes recently sent to the client. + + The done event establishes and updates the state of the client, and + must be issued after any changes to apply them. + + Text input state (content purpose, content hint, surrounding text, and + change cause) is conceptually double-buffered within an input method + context. + + Events modify the pending state, as opposed to the current state in use + by the input method. A done event atomically applies all pending state, + replacing the current state. After done, the new pending state is as + documented for each related request. + + Events must be applied in the order of arrival. + + Neither current nor pending state are modified unless noted otherwise. + + + + + + Send the commit string text for insertion to the application. + + Inserts a string at current cursor position (see commit event + sequence). The string to commit could be either just a single character + after a key press or the result of some composing. + + The argument text is a buffer containing the string to insert. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_text_input_v3.commit request. + + The initial value of text is an empty string. + + + + + + + Send the pre-edit string text to the application text input. + + Place a new composing text (pre-edit) at the current cursor position. + Any previously set composing text must be removed. Any previously + existing selected text must be removed. The cursor is moved to a new + position within the preedit string. + + The argument text is a buffer containing the preedit string. There is + a maximum length of wayland messages, so text can not be longer than + 4000 bytes. + + The arguments cursor_begin and cursor_end are counted in bytes relative + to the beginning of the submitted string buffer. Cursor should be + hidden by the text input when both are equal to -1. + + cursor_begin indicates the beginning of the cursor. cursor_end + indicates the end of the cursor. It may be equal or different than + cursor_begin. + + Values set with this event are double-buffered. They must be applied on + the next zwp_input_method_v2.commit event. + + The initial value of text is an empty string. The initial value of + cursor_begin, and cursor_end are both 0. + + + + + + + + + Remove the surrounding text. + + before_length and after_length are the number of bytes before and after + the current cursor index (excluding the preedit text) to delete. + + If any preedit text is present, it is replaced with the cursor for the + purpose of this event. In effect before_length is counted from the + beginning of preedit text, and after_length from its end (see commit + event sequence). + + Values set with this event are double-buffered. They must be applied + and reset to initial on the next zwp_input_method_v2.commit request. + + The initial values of both before_length and after_length are 0. + + + + + + + + Apply state changes from commit_string, set_preedit_string and + delete_surrounding_text requests. + + The state relating to these events is double-buffered, and each one + modifies the pending state. This request replaces the current state + with the pending state. + + The connected text input is expected to proceed by evaluating the + changes in the following order: + + 1. Replace existing preedit string with the cursor. + 2. Delete requested surrounding text. + 3. Insert commit string with the cursor at its end. + 4. Calculate surrounding text to send. + 5. Insert new preedit text in cursor position. + 6. Place cursor inside preedit text. + + The serial number reflects the last state of the zwp_input_method_v2 + object known to the client. The value of the serial argument must be + equal to the number of done events already issued by that object. When + the compositor receives a commit request with a serial different than + the number of past done events, it must proceed as normal, except it + should not change the current state of the zwp_input_method_v2 object. + + + + + + + Creates a new zwp_input_popup_surface_v2 object wrapping a given + surface. + + The surface gets assigned the "input_popup" role. If the surface + already has an assigned role, the compositor must issue a protocol + error. + + + + + + + + Allow an input method to receive hardware keyboard input and process + key events to generate text events (with pre-edit) over the wire. This + allows input methods which compose multiple key events for inputting + text like it is done for CJK languages. + + The compositor should send all keyboard events on the seat to the grab + holder via the returned wl_keyboard object. Nevertheless, the + compositor may decide not to forward any particular event. The + compositor must not further process any event after it has been + forwarded to the grab holder. + + Releasing the resulting wl_keyboard object releases the grab. + + + + + + + The input method ceased to be available. + + The compositor must issue this event as the only event on the object if + there was another input_method object associated with the same seat at + the time of its creation. + + The compositor must issue this request when the object is no longer + useable, e.g. due to seat removal. + + The input method context becomes inert and should be destroyed after + deactivation is handled. Any further requests and events except for the + destroy request must be ignored. + + + + + + Destroys the zwp_text_input_v2 object and any associated child + objects, i.e. zwp_input_popup_surface_v2 and + zwp_input_method_keyboard_grab_v2. + + + + + + + This interface marks a surface as a popup for interacting with an input + method. + + The compositor should place it near the active text input area. It must + be visible if and only if the input method is in the active state. + + The client must not destroy the underlying wl_surface while the + zwp_input_popup_surface_v2 object exists. + + + + + Notify about the position of the area of the text input expressed as a + rectangle in surface local coordinates. + + This is a hint to the input method telling it the relative position of + the text being entered. + + + + + + + + + + + + + + The zwp_input_method_keyboard_grab_v2 interface represents an exclusive + grab of the wl_keyboard interface associated with the seat. + + + + + This event provides a file descriptor to the client which can be + memory-mapped to provide a keyboard mapping description. + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. + + + + + + + + + + Notifies clients that the modifier and/or group state has changed, and + it should update its local state. + + + + + + + + + + + + + + + Informs the client about the keyboard's repeat rate and delay. + + This event is sent as soon as the zwp_input_method_keyboard_grab_v2 + object has been created, and is guaranteed to be received by the + client before any key press event. + + Negative values for either rate or delay are illegal. A rate of zero + will disable any repeating (regardless of the value of delay). + + This event can be sent later on as well with a new value if necessary, + so clients should continue listening for the event past the creation + of zwp_input_method_keyboard_grab_v2. + + + + + + + + + The input method manager allows the client to become the input method on + a chosen seat. + + No more than one input method must be associated with any seat at any + given time. + + + + + Request a new input zwp_input_method_v2 object associated with a given + seat. + + + + + + + + Destroys the zwp_input_method_manager_v2 object. + + The zwp_input_method_v2 objects originating from it remain valid. + + + + diff --git a/protocol/meson.build b/protocol/meson.build new file mode 100644 index 000000000..3007a1c17 --- /dev/null +++ b/protocol/meson.build @@ -0,0 +1,48 @@ +fs = import('fs') + +proto_inc = include_directories('.') + +wl_protocol_dir = wayland_protos_dep.get_variable(pkgconfig: 'pkgdatadir') + +wayland_scanner = find_program('wayland-scanner') + +wl_protos = [ + [wl_protocol_dir, 'stable/xdg-shell/xdg-shell.xml'], + [wl_protocol_dir, 'staging/ext-idle-notify/ext-idle-notify-v1.xml'], + [wl_protocol_dir, 'unstable/xdg-output/xdg-output-unstable-v1.xml'], + ['input-method-unstable-v2.xml'], + ['phoc-device-state-unstable-v1.xml'], + ['phoc-layer-shell-effects-unstable-v1.xml'], + ['phosh-private.xml'], + ['virtual-keyboard-unstable-v1.xml'], + ['wlr-foreign-toplevel-management-unstable-v1.xml'], + ['wlr-gamma-control-unstable-v1.xml'], + ['wlr-layer-shell-unstable-v1.xml'], + ['wlr-output-management-unstable-v1.xml'], + ['wlr-output-power-management-unstable-v1.xml'], + ['wlr-screencopy-unstable-v1.xml'], +] + +wl_proto_headers = [] +wl_proto_headers = [] +wl_proto_sources = [] + +foreach p : wl_protos + xml = join_paths(p) + + base = fs.name(xml) + proto = fs.stem(base) + + wl_proto_headers += custom_target( + '@0@ client header'.format(proto), + input: xml, + output: '@0@-client-protocol.h'.format(proto), + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], + ) + wl_proto_sources += custom_target( + '@0@ source'.format(proto), + input: xml, + output: '@0@-protocol.c'.format(proto), + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], + ) +endforeach diff --git a/protocol/phoc-device-state-unstable-v1.xml b/protocol/phoc-device-state-unstable-v1.xml new file mode 100644 index 000000000..9bb91526b --- /dev/null +++ b/protocol/phoc-device-state-unstable-v1.xml @@ -0,0 +1,153 @@ + + + + Copyright (C) 2023 Phosh.mobi e.V. + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Phones, tablets, convertibles, laptops can have additional hardware attached or switch + their operation mode from e.g. tablet to laptop. This protocol is meant to provide information + about these changes to interested clients. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + This protocol is meant to collect necessary bits before we propose an + upstream solution. + + + + + This is a bitmask of capabilities this devices has; if a member is + set, then the hardware is present. + + + + + + + + These errors can be emitted in response to zphoc_device_state_v1 requests. + + + + + + + This is emitted whenever a device gains or loses a capbility. + The argument is a capability enum containing the complete set + of hw capabilities this device has. + + + + + + + The ID provided will be initialized to the phoc_tablet_mode_switch interface + for this device + + This request only takes effect if the seat has the tablet-mode-switch + capability, or has had the tablet-mode-switch capability in the past. + It is a protocol violation to issue this request on a seat that has + never had the tablet-mode-switch capability. The + missing_capability error will be sent in this case. + + + + + + + The ID provided will be initialized to the phoc_lid_switch interface + for this device + + This request only takes effect if the seat has the lid-switch + capability, or has had the lid-switch capability in the past. + It is a protocol violation to issue this request on a seat that has + never had the tablet-mode-switch capability. The + missing_capability error will be sent in this case. + + + + + + + + + The tablet_mode_switch interface represents a tablet mode switch. + It can have two possible values: + + The wl_pointer interface generates enabled and disabled events to indicate + switch state changes + + + + + Using this request a client can tell the server that it is not going to + use the switch object anymore. + + + + + + + + + + + + + + + The lid_switch interface represents a tablet mode switch. + It can have two possible values: + + The wl_pointer interface generates enabled and disabled events to indicate + switch state changes + + + + + Using this request a client can tell the server that it is not going to + use the switch object anymore. + + + + + + + + + + + + + diff --git a/protocol/phoc-layer-shell-effects-unstable-v1.xml b/protocol/phoc-layer-shell-effects-unstable-v1.xml new file mode 100644 index 000000000..2cc900835 --- /dev/null +++ b/protocol/phoc-layer-shell-effects-unstable-v1.xml @@ -0,0 +1,288 @@ + + + + Copyright © 2022-2024 Guido Günther + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Assigns additional features to layer surfaces such as opacity or gestures interaction. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + This protocol is meant to collect necessary bits before we propose an + upstream solution. + + + + + + + + + + + + + + + + This marks a layer surface as draggable via one finger drag on touch or click and + drag when using a mouse. E.g. a surface that is anchored to the left, right and + top can be dragged to the bottom exposing more of it's surface. + + The state with the minimal amount of surface visible is called + folded state, the state with the maximum amount of surface visible is + called unfolded state. These two states are defined by the layer + surfaces margin in those states. During drag the layer surfaces margin + is adjusted to move the surface invalidating the margin set on the + zwlr_layer_surface_v1 interface. The default folded and unfolded + margins are 0. + + The threshold ([0.0, 1.0]) specifies how far the user needs to + pull out the surface to not slide back into the folded state but + rather slide into the unfolded state. The default is 1.0. + + The client is responsible for adjusting it's margins when it's + size changes to e.g. make sure only the wanted parts of the surface + are visible in folded state. + + Note that you shouldn't modify exclusive_zone or margins via + zwlr_layer_surface_v1 interface directly when using this one. + + Margins, threshold and exclusive zone are double buffered and will + be applied on the next surface commit. + + + + + + + + This request indicates that the client will not use the layer_shell + effects object any more. Objects that have been created through this instance + are not affected. + + + + + + + + + + + + Making a layer surface stacked allows to fixate it's position in + the layer surface stack. + + + + + + + + + + An interface that may be implemented by a layer_surface, for surfaces that + are designed to become draggable when anchored at three edges. + + + + + Sets the folded and unfolded margins. For how they are defined + refer to margins in the layer-surface protocol. + + Margins are double-buffered, see wl_surface.commit. + + + + + + + + How far the surface needs to be pulled to go from folded to unfolded state + and vice versa. Ranges 0.0 to 1.0 as fraction of the total margin + between folded and unfolded state. + + Threshold is double-buffered, see wl_surface.commit. + + + + + + + Sets the exclusive zone. This is the amount of pixels from the screen + edge that shouldn't be occluded by other surfaces. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Specify how the surface can be dragged. The default is + "full". + + Drag-mode is double-buffered, see wl_surface.commit. + + + + + + + Specify the position of the drag handle and thus the area that responds + to drag gestures. + + If the surface's non anchored edge is at the left or right it specifies + the x, otherwise the y coordinate of the drag handle in surface local + coordinates. The area between the non anchored surface edge and the + drag handle responds to drag gestures. + + 0 +-----------------+ + 1 | | + 2 | A | + 3 | | + 4 +++++++++++++++++++ -- drag handle + 5 | | + 6 | | + 7 | B | + 8 | | + 9 +-----------------+ + + If the above surface's non anchored edge is at the bottom then a drag handle + of 4 will make area B respond to touch events. If the non anchored edge is at + the top the same value makes area A respond to touch events. + + A special value of 0 indicates that the whole surface should be draggable. + + The default is 0. + + Drag-handle is double-buffered, see wl_surface.commit. + + + + + + + Set state to folded or unfolded. This is applied immediately. + + + + + + + This request destroys the draggable layer surface. + + It's a protocol error if the client destroys the layer-surface before + the draggable layer-surface. + + + + + + The is draging the surface. + + TODO: indicate drag (by user) vs slide (by compositor) + + + + + + + The user ended dragging the surface and it entered either folded + or unfolded state. + + + + + + + + + + + + + + + + + + + + + + Set the surface's transparency. Valid values are between 0.0 + (fully transparent) and 1.0 (fully opaque). The set value + also affects subsurfaces and popups. + + Alpha is double-buffered, see wl_surface.commit. + + + + + + + This request destroys the alpha layer surface. + + + + + + + + + + Attaches the stacked layer surface below the layer surface given + in this request. + + + + + + + Attaches the stacked layer surface above the layer surface given + in this request. + + + + + + + This request destroys the layer surface binder. + + + + + + diff --git a/protocol/phosh-private.xml b/protocol/phosh-private.xml new file mode 100644 index 000000000..e64e0b7e1 --- /dev/null +++ b/protocol/phosh-private.xml @@ -0,0 +1,240 @@ + + + + Private protocol between phosh and the compositor. + + + + + + + + + This request is unused, ignore. Use wlr-output-management instead. + + + + + + + + This request is unused, ignore. Use wlr-foreign-toplevel-management instead. + + + + + + + Allows to retrieve a window thumbnail image for a given foreign + toplevel via wlr_screencopy protocol. + + The thumbnail will be scaled down to the size provided by + max_width and max_height arguments, preserving original aspect + ratio. Pass 0 to leave it unconstrained. + + + + + + + + + + Allows to subscribe to specific keyboard events. + + The client grabs an accelerator by a string and gets an action id returned. + When the accelerator is used the client will be informed via the corresponding + action id. + + + + + + + Allows to track application startup. + + + + + + + + + + + + This allows the shell to report it's current state. This can + e.g. be used to notify the compositor that the shell is up. + + + + + + + + + The interface is meant to allow subscription and forwarding of keyboard events. + + + + + + + + + + The subscribed accelerator has been pressed. + + + + + + + + A previous accelerator grab request has failed. + + + + + + + + A previous accelerator grab request has succeeded. + + + + + + + + Client subscribes to a specific accelerator. + + + + + + + A previous accelerator ungrab request has failed. + + + + + + + + A previous accelerator ungrab request has suceeded. + + + + + + + Client unsubscribes a specific accelerator" + + + + + + + + + + + + The subscribed accelerator has been released. + + + + + + + + + + This interface is unused, ignore. Use wlr-foreign-toplevel-management instead. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Allows shells to track application startup. + + + + + + + + + + + + + + + + This event indicates that the client sent it's startup id. + (which implies the app is running). + + + + + + + + + This event indicates that the launcher spawned the app. + + + + + + + + + The Client should invoke this when done using the interface. + + + + diff --git a/protocol/virtual-keyboard-unstable-v1.xml b/protocol/virtual-keyboard-unstable-v1.xml new file mode 100644 index 000000000..df4d01cef --- /dev/null +++ b/protocol/virtual-keyboard-unstable-v1.xml @@ -0,0 +1,113 @@ + + + + Copyright © 2008-2011 Kristian Høgsberg + Copyright © 2010-2013 Intel Corporation + Copyright © 2012-2013 Collabora, Ltd. + Copyright © 2018 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + + The virtual keyboard provides an application with requests which emulate + the behaviour of a physical keyboard. + + This interface can be used by clients on its own to provide raw input + events, or it can accompany the input method protocol. + + + + + Provide a file descriptor to the compositor which can be + memory-mapped to provide a keyboard mapping description. + + Format carries a value from the keymap_format enumeration. + + + + + + + + + + + + + A key was pressed or released. + The time argument is a timestamp with millisecond granularity, with an + undefined base. All requests regarding a single object must share the + same clock. + + Keymap must be set before issuing this request. + + State carries a value from the key_state enumeration. + + + + + + + + + Notifies the compositor that the modifier and/or group state has + changed, and it should update state. + + The client should use wl_keyboard.modifiers event to synchronize its + internal state with seat state. + + Keymap must be set before issuing this request. + + + + + + + + + + + + + + + A virtual keyboard manager allows an application to provide keyboard + input events as if they came from a physical keyboard. + + + + + + + + + Creates a new virtual keyboard associated to a seat. + + If the compositor enables a keyboard to perform arbitrary actions, it + should present an error when an untrusted client requests a new + keyboard. + + + + + + diff --git a/protocol/wlr-foreign-toplevel-management-unstable-v1.xml b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 000000000..108133715 --- /dev/null +++ b/protocol/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + + diff --git a/protocol/wlr-gamma-control-unstable-v1.xml b/protocol/wlr-gamma-control-unstable-v1.xml new file mode 100644 index 000000000..16e0be8b1 --- /dev/null +++ b/protocol/wlr-gamma-control-unstable-v1.xml @@ -0,0 +1,126 @@ + + + + Copyright © 2015 Giulio camuffo + Copyright © 2018 Simon Ser + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows a privileged client to set the gamma tables for + outputs. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-output gamma + controls. + + + + + Create a gamma control that can be used to adjust gamma tables for the + provided output. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This interface allows a client to adjust gamma tables for a particular + output. + + The client will receive the gamma size, and will then be able to set gamma + tables. At any time the compositor can send a failed event indicating that + this object is no longer valid. + + There can only be at most one gamma control object per output, which + has exclusive access to this particular output. When the gamma control + object is destroyed, the gamma table is restored to its original value. + + + + + Advertise the size of each gamma ramp. + + This event is sent immediately when the gamma control object is created. + + + + + + + + + + + Set the gamma table. The file descriptor can be memory-mapped to provide + the raw gamma table, which contains successive gamma ramps for the red, + green and blue channels. Each gamma ramp is an array of 16-byte unsigned + integers which has the same length as the gamma size. + + The file descriptor data must have the same length as three times the + gamma size. + + + + + + + This event indicates that the gamma control is no longer valid. This + can happen for a number of reasons, including: + - The output doesn't support gamma tables + - Setting the gamma tables failed + - Another client already has exclusive gamma control for this output + - The compositor has transferred gamma control to another client + + Upon receiving this event, the client should destroy this object. + + + + + + Destroys the gamma control object. If the object is still valid, this + restores the original gamma tables. + + + + diff --git a/protocol/wlr-layer-shell-unstable-v1.xml b/protocol/wlr-layer-shell-unstable-v1.xml new file mode 100644 index 000000000..d62fd51e9 --- /dev/null +++ b/protocol/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,390 @@ + + + + Copyright © 2017 Drew DeVault + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + Clients can use this interface to assign the surface_layer role to + wl_surfaces. Such surfaces are assigned to a "layer" of the output and + rendered with a defined z-depth respective to each other. They may also be + anchored to the edges and corners of a screen and specify input handling + semantics. This interface should be suitable for the implementation of + many desktop shell components, and a broad number of other applications + that interact with the desktop. + + + + + Create a layer surface for an existing surface. This assigns the role of + layer_surface, or raises a protocol error if another role is already + assigned. + + Creating a layer surface from a wl_surface which has a buffer attached + or committed is a client error, and any attempts by a client to attach + or manipulate a buffer prior to the first layer_surface.configure call + must also be treated as errors. + + After creating a layer_surface object and setting it up, the client + must perform an initial commit without any buffer attached. + The compositor will reply with a layer_surface.configure event. + The client must acknowledge it and is then allowed to attach a buffer + to map the surface. + + You may pass NULL for output to allow the compositor to decide which + output to use. Generally this will be the one that the user most + recently interacted with. + + Clients can specify a namespace that defines the purpose of the layer + surface. + + + + + + + + + + + + + + + + + These values indicate which layers a surface can be rendered in. They + are ordered by z depth, bottom-most first. Traditional shell surfaces + will typically be rendered between the bottom and top layers. + Fullscreen shell surfaces are typically rendered at the top layer. + Multiple surfaces can share a single layer, and ordering within a + single layer is undefined. + + + + + + + + + + + + + This request indicates that the client will not use the layer_shell + object any more. Objects that have been created through this instance + are not affected. + + + + + + + An interface that may be implemented by a wl_surface, for surfaces that + are designed to be rendered as a layer of a stacked desktop-like + environment. + + Layer surface state (layer, size, anchor, exclusive zone, + margin, interactivity) is double-buffered, and will be applied at the + time wl_surface.commit of the corresponding wl_surface is called. + + Attaching a null buffer to a layer surface unmaps it. + + Unmapping a layer_surface means that the surface cannot be shown by the + compositor until it is explicitly mapped again. The layer_surface + returns to the state it had right after layer_shell.get_layer_surface. + The client can re-map the surface by performing a commit without any + buffer attached, waiting for a configure event and handling it as usual. + + + + + Sets the size of the surface in surface-local coordinates. The + compositor will display the surface centered with respect to its + anchors. + + If you pass 0 for either value, the compositor will assign it and + inform you of the assignment in the configure event. You must set your + anchor to opposite edges in the dimensions you omit; not doing so is a + protocol error. Both values are 0 by default. + + Size is double-buffered, see wl_surface.commit. + + + + + + + + Requests that the compositor anchor the surface to the specified edges + and corners. If two orthogonal edges are specified (e.g. 'top' and + 'left'), then the anchor point will be the intersection of the edges + (e.g. the top left corner of the output); otherwise the anchor point + will be centered on that edge, or in the center if none is specified. + + Anchor is double-buffered, see wl_surface.commit. + + + + + + + Requests that the compositor avoids occluding an area with other + surfaces. The compositor's use of this information is + implementation-dependent - do not assume that this region will not + actually be occluded. + + A positive value is only meaningful if the surface is anchored to one + edge or an edge and both perpendicular edges. If the surface is not + anchored, anchored to only two perpendicular edges (a corner), anchored + to only two parallel edges or anchored to all edges, a positive value + will be treated the same as zero. + + A positive zone is the distance from the edge in surface-local + coordinates to consider exclusive. + + Surfaces that do not wish to have an exclusive zone may instead specify + how they should interact with surfaces that do. If set to zero, the + surface indicates that it would like to be moved to avoid occluding + surfaces with a positive exclusive zone. If set to -1, the surface + indicates that it would not like to be moved to accommodate for other + surfaces, and the compositor should extend it all the way to the edges + it is anchored to. + + For example, a panel might set its exclusive zone to 10, so that + maximized shell surfaces are not shown on top of it. A notification + might set its exclusive zone to 0, so that it is moved to avoid + occluding the panel, but shell surfaces are shown underneath it. A + wallpaper or lock screen might set their exclusive zone to -1, so that + they stretch below or over the panel. + + The default value is 0. + + Exclusive zone is double-buffered, see wl_surface.commit. + + + + + + + Requests that the surface be placed some distance away from the anchor + point on the output, in surface-local coordinates. Setting this value + for edges you are not anchored to has no effect. + + The exclusive zone includes the margin. + + Margin is double-buffered, see wl_surface.commit. + + + + + + + + + + Types of keyboard interaction possible for layer shell surfaces. The + rationale for this is twofold: (1) some applications are not interested + in keyboard events and not allowing them to be focused can improve the + desktop experience; (2) some applications will want to take exclusive + keyboard focus. + + + + + This value indicates that this surface is not interested in keyboard + events and the compositor should never assign it the keyboard focus. + + This is the default value, set for newly created layer shell surfaces. + + This is useful for e.g. desktop widgets that display information or + only have interaction with non-keyboard input devices. + + + + + Request exclusive keyboard focus if this surface is above the shell surface layer. + + For the top and overlay layers, the seat will always give + exclusive keyboard focus to the top-most layer which has keyboard + interactivity set to exclusive. If this layer contains multiple + surfaces with keyboard interactivity set to exclusive, the compositor + determines the one receiving keyboard events in an implementation- + defined manner. In this case, no guarantee is made when this surface + will receive keyboard focus (if ever). + + For the bottom and background layers, the compositor is allowed to use + normal focus semantics. + + This setting is mainly intended for applications that need to ensure + they receive all keyboard events, such as a lock screen or a password + prompt. + + + + + This requests the compositor to allow this surface to be focused and + unfocused by the user in an implementation-defined manner. The user + should be able to unfocus this surface even regardless of the layer + it is on. + + Typically, the compositor will want to use its normal mechanism to + manage keyboard focus between layer shell surfaces with this setting + and regular toplevels on the desktop layer (e.g. click to focus). + Nevertheless, it is possible for a compositor to require a special + interaction to focus or unfocus layer shell surfaces (e.g. requiring + a click even if focus follows the mouse normally, or providing a + keybinding to switch focus between layers). + + This setting is mainly intended for desktop shell components (e.g. + panels) that allow keyboard interaction. Using this option can allow + implementing a desktop shell that can be fully usable without the + mouse. + + + + + + + Set how keyboard events are delivered to this surface. By default, + layer shell surfaces do not receive keyboard events; this request can + be used to change this. + + This setting is inherited by child surfaces set by the get_popup + request. + + Layer surfaces receive pointer, touch, and tablet events normally. If + you do not want to receive them, set the input region on your surface + to an empty region. + + Keyboard interactivity is double-buffered, see wl_surface.commit. + + + + + + + This assigns an xdg_popup's parent to this layer_surface. This popup + should have been created via xdg_surface::get_popup with the parent set + to NULL, and this request must be invoked before committing the popup's + initial state. + + See the documentation of xdg_popup for more details about what an + xdg_popup is and how it is used. + + + + + + + When a configure event is received, if a client commits the + surface in response to the configure event, then the client + must make an ack_configure request sometime before the commit + request, passing along the serial of the configure event. + + If the client receives multiple configure events before it + can respond to one, it only has to ack the last configure event. + + A client is not required to commit immediately after sending + an ack_configure request - it may even ack_configure several times + before its next surface commit. + + A client may send multiple ack_configure requests before committing, but + only the last request sent before a commit indicates which configure + event the client really is responding to. + + + + + + + This request destroys the layer surface. + + + + + + The configure event asks the client to resize its surface. + + Clients should arrange their surface for the new states, and then send + an ack_configure request with the serial sent in this configure event at + some point before committing the new surface. + + The client is free to dismiss all but the last configure event it + received. + + The width and height arguments specify the size of the window in + surface-local coordinates. + + The size is a hint, in the sense that the client is free to ignore it if + it doesn't resize, pick a smaller size (to satisfy aspect ratio or + resize in steps of NxM pixels). If the client picks a smaller size and + is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the + surface will be centered on this axis. + + If the width or height arguments are zero, it means the client should + decide its own window dimension. + + + + + + + + + The closed event is sent by the compositor when the surface will no + longer be shown. The output may have been destroyed or the user may + have asked for it to be removed. Further changes to the surface will be + ignored. The client should destroy the resource after receiving this + event, and create a new surface if they so choose. + + + + + + + + + + + + + + + + + + + + + + Change the layer that the surface is rendered on. + + Layer is double-buffered, see wl_surface.commit. + + + + + diff --git a/protocol/wlr-output-management-unstable-v1.xml b/protocol/wlr-output-management-unstable-v1.xml new file mode 100644 index 000000000..cadc45fb2 --- /dev/null +++ b/protocol/wlr-output-management-unstable-v1.xml @@ -0,0 +1,554 @@ + + + + Copyright © 2019 Purism SPC + + 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, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol exposes interfaces to obtain and modify output device + configuration. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows reading and writing the current + output device configuration. + + Output devices that display pixels (e.g. a physical monitor or a virtual + output in a window) are represented as heads. Heads cannot be created nor + destroyed by the client, but they can be enabled or disabled and their + properties can be changed. Each head may have one or more available modes. + + Whenever a head appears (e.g. a monitor is plugged in), it will be + advertised via the head event. Immediately after the output manager is + bound, all current heads are advertised. + + Whenever a head's properties change, the relevant wlr_output_head events + will be sent. Not all head properties will be sent: only properties that + have changed need to. + + Whenever a head disappears (e.g. a monitor is unplugged), a + wlr_output_head.finished event will be sent. + + After one or more heads appear, change or disappear, the done event will + be sent. It carries a serial which can be used in a create_configuration + request to update heads properties. + + The information obtained from this protocol should only be used for output + configuration purposes. This protocol is not designed to be a generic + output property advertisement protocol for regular clients. Instead, + protocols such as xdg-output should be used. + + + + + This event introduces a new head. This happens whenever a new head + appears (e.g. a monitor is plugged in) or after the output manager is + bound. + + + + + + + This event is sent after all information has been sent after binding to + the output manager object and after any subsequent changes. This applies + to child head and mode objects as well. In other words, this event is + sent whenever a head or mode is created or destroyed and whenever one of + their properties has been changed. Not all state is re-sent each time + the current configuration changes: only the actual changes are sent. + + This allows changes to the output configuration to be seen as atomic, + even if they happen via multiple events. + + A serial is sent to be used in a future create_configuration request. + + + + + + + Create a new output configuration object. This allows to update head + properties. + + + + + + + + Indicates the client no longer wishes to receive events for output + configuration changes. However the compositor may emit further events, + until the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending manager events. + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + A head is an output device. The difference between a wl_output object and + a head is that heads are advertised even if they are turned off. A head + object only advertises properties and cannot be used directly to change + them. + + A head has some read-only properties: modes, name, description and + physical_size. These cannot be changed by clients. + + Other properties can be updated via a wlr_output_configuration object. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the head name. + + The naming convention is compositor defined, but limited to alphanumeric + characters and dashes (-). Each name is unique among all wlr_output_head + objects, but if a wlr_output_head object is destroyed the same name may + be reused later. The names will also remain consistent across sessions + with the same hardware and software configuration. + + Examples of names include 'HDMI-A-1', 'WL-1', 'X11-1', etc. However, do + not assume that the name is a reflection of an underlying DRM + connector, X11 connection, etc. + + If the compositor implements the xdg-output protocol and this head is + enabled, the xdg_output.name event must report the same name. + + The name event is sent after a wlr_output_head object is created. This + event is only sent once per object, and the name does not change over + the lifetime of the wlr_output_head object. + + + + + + + This event describes a human-readable description of the head. + + The description is a UTF-8 string with no convention defined for its + contents. Examples might include 'Foocorp 11" Display' or 'Virtual X11 + output via :1'. However, do not assume that the name is a reflection of + the make, model, serial of the underlying DRM connector or the display + name of the underlying X11 connection, etc. + + If the compositor implements xdg-output and this head is enabled, + the xdg_output.description must report the same description. + + The description event is sent after a wlr_output_head object is created. + This event is only sent once per object, and the description does not + change over the lifetime of the wlr_output_head object. + + + + + + + This event describes the physical size of the head. This event is only + sent if the head has a physical size (e.g. is not a projector or a + virtual device). + + + + + + + + This event introduces a mode for this head. It is sent once per + supported mode. + + + + + + + This event describes whether the head is enabled. A disabled head is not + mapped to a region of the global compositor space. + + When a head is disabled, some properties (current_mode, position, + transform and scale) are irrelevant. + + + + + + + This event describes the mode currently in use for this head. It is only + sent if the output is enabled. + + + + + + + This events describes the position of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + + This event describes the transformation currently applied to the head. + It is only sent if the output is enabled. + + + + + + + This events describes the scale of the head in the global compositor + space. It is only sent if the output is enabled. + + + + + + + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + This event describes the manufacturer of the head. + + This must report the same make as the wl_output interface does in its + geometry event. + + Together with the model and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the make of + the head or the definition of a make is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the make string in UI to users. For + that the string provided by the description event should be preferred. + + + + + + + This event describes the model of the head. + + This must report the same model as the wl_output interface does in its + geometry event. + + Together with the make and serial_number events the purpose is to + allow clients to recognize heads from previous sessions and for example + load head-specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the model of + the head or the definition of a model is not sensible in the current + setup, for example in a virtual session. Clients can still try to + identify the head by available information from other events but should + be aware that there is an increased risk of false positives. + + It is not recommended to display the model string in UI to users. For + that the string provided by the description event should be preferred. + + + + + + + This event describes the serial number of the head. + + Together with the make and model events the purpose is to allow clients + to recognize heads from previous sessions and for example load head- + specific configurations back. + + It is not guaranteed this event will be ever sent. A reason for that + can be that the compositor does not have information about the serial + number of the head or the definition of a serial number is not sensible + in the current setup. Clients can still try to identify the head by + available information from other events but should be aware that there + is an increased risk of false positives. + + It is not recommended to display the serial_number string in UI to + users. For that the string provided by the description event should be + preferred. + + + + + + + + This object describes an output mode. + + Some heads don't support output modes, in which case modes won't be + advertised. + + Properties sent via this interface are applied atomically via the + wlr_output_manager.done event. No guarantees are made regarding the order + in which properties are sent. + + + + + This event describes the mode size. The size is given in physical + hardware units of the output device. This is not necessarily the same as + the output size in the global compositor space. For instance, the output + may be scaled or transformed. + + + + + + + + This event describes the mode's fixed vertical refresh rate. It is only + sent if the mode has a fixed refresh rate. + + + + + + + This event advertises this mode as preferred. + + + + + + The compositor will destroy the object immediately after sending this + event, so it will become invalid and the client should release any + resources associated with it. + + + + + + + This object is used by the client to describe a full output configuration. + + First, the client needs to setup the output configuration. Each head can + be either enabled (and configured) or disabled. It is a protocol error to + send two enable_head or disable_head requests with the same head. It is a + protocol error to omit a head in a configuration. + + Then, the client can apply or test the configuration. The compositor will + then reply with a succeeded, failed or cancelled event. Finally the client + should destroy the configuration object. + + + + + + + + + + + Enable a head. This request creates a head configuration object that can + be used to change the head's properties. + + + + + + + + Disable a head. + + + + + + + Apply the new output configuration. + + In case the configuration is successfully applied, there is no guarantee + that the new output state matches completely the requested + configuration. For instance, a compositor might round the scale if it + doesn't support fractional scaling. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Test the new output configuration. The configuration won't be applied, + but will only be validated. + + Even if the compositor succeeds to test a configuration, applying it may + fail. + + After this request has been sent, the compositor must respond with an + succeeded, failed or cancelled event. Sending a request that isn't the + destructor is a protocol error. + + + + + + Sent after the compositor has successfully applied the changes or + tested them. + + Upon receiving this event, the client should destroy this object. + + If the current configuration has changed, events to describe the changes + will be sent followed by a wlr_output_manager.done event. + + + + + + Sent if the compositor rejects the changes or failed to apply them. The + compositor should revert any changes made by the apply request that + triggered this event. + + Upon receiving this event, the client should destroy this object. + + + + + + Sent if the compositor cancels the configuration because the state of an + output changed and the client has outdated information (e.g. after an + output has been hotplugged). + + The client can create a new configuration with a newer serial and try + again. + + Upon receiving this event, the client should destroy this object. + + + + + + Using this request a client can tell the compositor that it is not going + to use the configuration object anymore. Any changes to the outputs + that have not been applied will be discarded. + + This request also destroys wlr_output_configuration_head objects created + via this object. + + + + + + + This object is used by the client to update a single head's configuration. + + It is a protocol error to set the same property twice. + + + + + + + + + + + + + This request sets the head's mode. + + + + + + + This request assigns a custom mode to the head. The size is given in + physical hardware units of the output device. If set to zero, the + refresh rate is unspecified. + + It is a protocol error to set both a mode and a custom mode. + + + + + + + + + This request sets the head's position in the global compositor space. + + + + + + + + This request sets the head's transform. + + + + + + + This request sets the head's scale. + + + + + diff --git a/protocol/wlr-output-power-management-unstable-v1.xml b/protocol/wlr-output-power-management-unstable-v1.xml new file mode 100644 index 000000000..a97783991 --- /dev/null +++ b/protocol/wlr-output-power-management-unstable-v1.xml @@ -0,0 +1,128 @@ + + + + Copyright © 2019 Purism SPC + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to control power management modes + of outputs that are currently part of the compositor space. The + intent is to allow special clients like desktop shells to power + down outputs when the system is idle. + + To modify outputs not currently part of the compositor space see + wlr-output-management. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-output power + management mode controls. + + + + + Create a output power management mode control that can be used to + adjust the power management mode for a given output. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object offers requests to set the power management mode of + an output. + + + + + + + + + + + + + + Set an output's power save mode to the given mode. The mode change + is effective immediately. If the output does not support the given + mode a failed event is sent. + + + + + + + Report the power management mode change of an output. + + The mode event is sent after an output changed its power + management mode. The reason can be a client using set_mode or the + compositor deciding to change an output's mode. + This event is also sent immediately when the object is created + so the client is informed about the current power management mode. + + + + + + + This event indicates that the output power management mode control + is no longer valid. This can happen for a number of reasons, + including: + - The output doesn't support power management + - Another client already has exclusive power management mode control + for this output + - The output disappeared + + Upon receiving this event, the client should destroy this object. + + + + + + Destroys the output power management mode control object. + + + + diff --git a/protocol/wlr-screencopy-unstable-v1.xml b/protocol/wlr-screencopy-unstable-v1.xml new file mode 100644 index 000000000..e4c21f80b --- /dev/null +++ b/protocol/wlr-screencopy-unstable-v1.xml @@ -0,0 +1,207 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Andri Yngvason + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to ask the compositor to copy part of the + screen content to a client buffer. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of an entire output. + + + + + + + + + Capture the next frame of an output's region. + + The region is given in output logical coordinates, see + xdg_output.logical_size. The region will be clipped to the output's + extents. + + + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object represents a single frame. + + When created, a "buffer" event will be sent. The client will then be able + to send a "copy" request. If the capture is successful, the compositor + will send a "flags" followed by a "ready" event. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about the frame's buffer. This event is sent once + as soon as the frame is created. + + The client should then create a buffer with the provided attributes, and + send a "copy" request. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have a the + correct size, see zwlr_screencopy_frame_v1.buffer. The buffer needs to + have a supported format. + + If the frame is successfully copied, a "flags" and a "ready" events are + sent. Otherwise, a "failed" event is sent. + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + + Same as copy, except it waits until there is damage to copy. + + + + + + + This event is sent right before the ready event when copy_with_damage is + requested. It may be generated multiple times for each copy_with_damage + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy_with_damage + and a ready event is the total damage since the prior ready event. + + + + + + + + diff --git a/run.in b/run.in new file mode 100755 index 000000000..091d4fb0e --- /dev/null +++ b/run.in @@ -0,0 +1,41 @@ +#!/bin/bash +set -e + +ABS_BUILDDIR='@ABS_BUILDDIR@' +ABS_SRCDIR='@ABS_SRCDIR@' + +if [ "${PHOSH_VALGRIND}" = 1 ]; then + echo "Running phosh under valgrind" + WRAPPER="valgrind -v \ + --tool=memcheck \ + --leak-check=full \ + --leak-resolution=high \ + --num-callers=40 \ + --log-file=vgdump \ + --suppressions=${ABS_SRCDIR}/data/valgrind.suppressions \ + --suppressions=/usr/share/glib-2.0/valgrind/glib.supp \ + " +elif [ "${PHOSH_GDB}" = 1 ]; then + echo "Running phosh under gdb" + WRAPPER="gdb --args" +elif [ "${PHOSH_AUTO_GDB}" = 1 ]; then + echo "Running phosh under gdb" + WRAPPER="gdb -batch -ex run -ex bt --args" +fi + +GSETTINGS_SCHEMA_DIR=${ABS_BUILDDIR}/data:@PLUGIN_SCHEMA_PATH@:${GSETTINGS_SCHEMA_DIR} +export GSETTINGS_SCHEMA_DIR + +[ -n "${G_MESSAGES_DEBUG}" ] || G_MESSAGES_DEBUG=all +export G_MESSAGES_DEBUG + +if [ -z "${GSETTINGS_BACKEND}" ]; then + # Make sure we don't mess with the systems gsettings: + export GSETTINGS_BACKEND=memory +fi + +# Ensure that we don't filter out wanted app launcher, i.e. p-m-s or g-c-c +export XDG_CURRENT_DESKTOP="Phosh:GNOME" + +set -x +exec ${WRAPPER} "${ABS_BUILDDIR}/src/phosh" -U $@ diff --git a/screenshots/phosh-locked.png b/screenshots/phosh-locked.png new file mode 100644 index 0000000000000000000000000000000000000000..b0035908b8cab3aedde39b9d5ee6cf5a225f933e GIT binary patch literal 9830 zcmdsdcTiO8mo4g5uK_S2S>ysL`I0l306}PSlpr8E$0nnqfRe5tG(kW~4Kz7Ora_>| zAX&1cMzYZ4Cd}?vZ~l2zZ>ru@&8y!`i6wQ8efst$p69BIL<0(p@AYBO_OM z@>rdW?35W9+3D^-&%qVnEEgsC=bY(N`Nw1@q(7f)(a~__g3}W{S28jl7Si8So*6P8 zaPhpGg0k%S*>l&=(ore)Ukiduv~IGxZjT)8?adwB$R4?vo4A?3xQ(=Sv$`#>pscF% zih`Pq>^7Og;|H3aqpK5M27~UYCmXvaG0NuG$5k87qW+L=G1Snc>*9JgP!la)Uf7u+ z(J!rm&0b5Y?B^QcuhiDoA2p37s#m_q_qxh@fyad*`%e^+OkUWVr%cC z5lK~R)|3pQ_v`BFc%EtV^73}2$f%?Q)EQ7p@2Tb$_%JasNo}@XmOA*w(%#)YRO3Y` zv*^;-%Nt6OdcD@aKz%T=?vL5q^PI=yZ8qn!6%`e8io80yyZa>8YcN_EZ3**{=%^^S zgDq>1`_uxL-Cg5^xqE-uX_xr!FIfEg{zb>vPE#}8>u6gid@#ZCtHXq;ewmGXBlmiR zGm<8_(ZPFXMQ@pKjf%<|f;5zlUKfeJJ7qaOF@X_EK0YGGmzHWqqmZ5Bu00b%{Ejz6 zn}4jH6%v*X5*H}Ewlw;4e%{>3XO+sV3?F~tI?lM?wc2gbR9!j>A;ff-uJupbjf&^kum zk|M3xv1X%ZnNH=!3yE2$AIj^KwIh}xe1Rp$l3(=+oh*@^SsymG*Cxi`FbD_B1%r$8 z6y)T5US2I-{VFnA>*X8cm99z;9vr)$#ro_fhBcaq7*>0jV_```QWcez@ktWpiRK-# zL%&j5RHU}kVa*ml>rc6Rj4(lRlLB5?Sm=#SlJHoux_HSRzrS#zyXw5PJUk5HKr=Q7 zT$w#5HSRu~gmd8idv&b5pm)RD(=%>vj6rl*7*gH5VJ{dq$IK{iK z(o1;cihHbN=@@wpvd?*9`I& z&i}E8si`r*{hrn{4Wqk$<;v$*XU^Nqx5proy5CcA&aJKGUu6jz{*pHHcx3hM!B)Qt zJaHX+@@lly8vW*t@z6*l;pWYoiczlvG1O)AO)b3*S9g-LO(7n6OjL7 zI?E^wE5nZ$Jj*8+eu0uw=lS!`u;$7a-afl`o<@ngFX>`Bq%1n~E>cns1ANiXc|=A; z#ImkVVX>p~I3edy%`0YyR_M;wR;RWt7gtv;BO`y4sxtG@(#IK@{^->)q+si+^o!Q# zjFO(JfZ~Ia{5u-f?DDuK8lgHV_J(k(ePctpUdK&_RZG><|MSM#W)#do^q~X7j zq9P*XG=JZJR_?E{>byrJy1q~fQ!C+HOE+tYsN6c7Y6v_}#W;~Y6f4HZr$!O{=qZZF z{+}^PW|Q>SuV0tGXbT>^bsqKgs@Xu{SNpNW)(>1L1mXLaM;9+$ibkQ93$jE5#Kyk8 z4b@ar>%CpRzqQyO^YNpbVT$M+dCkE)$KZU~aC#AK1R~-oTF=11*CaT9t$Ke@$ZPY3 zmuJQDYB@FRjgg){?bMkA+tJeV1-6p@{{AkmC0X*3x8A>huc@i|F*P-HAXE9x>C>kN zDxA&E{_#g^ON%@-7jfs*C$h44KPDz(02pjrB9au9mFE*N@A!D6zp-<>uWqN_Rq>ap zefg3Mh1-KFv>nQI-{<{YH4|@{tR^QHBu!i)Ec;IUVRf*zm>}&}1+jMr1TnW<__bV6 zo>NhwD=XW~!NU{VY^wJBxrUJas3!EwrAaBd9%*9mr4L4>Rv#vP3Ebx9jOLcPEEQ(z z+~kXk=R2p4cXS|1hm2SQKeJ5&(h}3LtL1@rTFy%g2NVM!uUm6gP7rKsYHrqAChkj0 zN-6W43bGJ=)o@rPhhKv%1{3nE$LC z=S%1sCU=n_9vme25u>`s9I<+6JjQtzhcCw-@j_D?_uZYSquZBlsOJ`R6kS_evl+*&CHnkZS}U0J4o!VPpR~y zWzz4a_~vx`Z%J@-E3?Q?rVb3Omp*S8Tk1zsO5EdfxPNykTuo0e{_yC?P<(>W?>a~y zyufSutn}B>Ii$eW=g+rEM4%(qFS9umHL&Q#&B@unw{cxlRrPcEl)r|wwDd}PQ^oW> zZtlD?niNT|KDPcue)IQ|Qc_z(&m4ua>}`v51#O4ubF#7q%p#@yA9a>=S@b*l6O3)3 zM_P1x#xZ`R5n%u*$Q|Y*RrWx(MybQZ!8zpqz#~ej^e>MbhmLk-eij@13Yie_+y_gT zf~{X^ky#4ScmCHmGti1SFsMG|^EkGCALNbvD(!dhD>A?r0lD!hDk{pMVn0QUK()WW z&o1pxBf-w+Kw~FdtzRDDv?%zj)apxW>R(#g+Twot{h4eWEG&-_MI0Ue9xHWU9@4M) zE#>#S!L%#o1W#UhSpvDH?(AH2?b@{;3h$g7oghQY?8oFLCntFlEzk(YvVfCQXq5%X zZxNe+avA}xn0yPLEG{m3Z?>Twe$0#d?BojDkETPR7e!y24Kg`7K7vfw2&Pyyq?tvC zJJs*=A(WH`0Ot#q*ItNR3vEuVbX{njnlg^Qt5P|=PWGUtEw#OL;aNISX## z%;LpUr%sUq*>C2%F>gmczh>GawVQj6@>cr0>vt;;&nb03FBzG`<1^tPJv2=XPRw60 z48UUt@}7yHl-9kmYYR5Y@%*wzM#pBee1??Aq9OBcmU?QN=?e=JLkK%RKXz*PI&4 zuPB%E(@DkQ{v?O->mINywX|qpO?r z8>rSRQKdE0)YWZ8D2CAo0oFf_>UaF!m#ZhxjKdkkC6_2A#%6VPHj?sTY1?BN*xOBB zf8zq|agX=Dpa1mqGzXX4ua%LKm2{q_)nyy6AGJOxy{~rJ8ffP!KP*2K_XTot=TSlL ztq>vT_^fxNRSU{>#=hEjzB*%#XFA#QsWrrLKNC)r4ZT?$;36{{BqT}b?8i+PCOS-f za&1J0Hk*=Ys`Sy;bu>>4iGQuEa6$W3wOw{O-1AY=9x^Gh8C0BIT-2MLbp$pyQnK)j z00e_hM5Nq%-}~~VO9Jr4O7Z;hN6|DQxy<+OwF2z8k9M^A&>867;$W9k)S@BkXuRcO*^b7-+*98f#LWq44uvALO3UeMCibY98iOBWPS0_mAG z)0GR!BpkDzs`Cqv^jRGp#FzBMin$-Bc+NO$=vuk99~v+BRstHHJ-eUjMZgOQPeRL% zAH?V+kkb#*8XHe}6UDc7c7(01v+;T5_!47CT_3kkNlE6hJV!rB-1Vfp8VI$v--x7fD9HZKZ-J;kqHSSC@CpRJU7l;5O*Avo(AI7w^3L$lAHM`m|_J5 z2tuKOM({yABtc;a@z)LIJxgSf=kHoBz~ZBL=^4db9@mcD0bbvkEY*>ozLM9=vRWm& z1-x}`o>An4hz+m^V8vaAKFmkjicW#iu%BSL$Er<VB}Y_)YMRiNa^w6 zf=ql^nA>$KDy;%zD|RQ};U*?(ZbjV!tNV4<`Ri(#6ck`5lP8$yV26#l`)i^scBX99C^w3 zxLtp`LbNxs{T}an9Ic?$^Q3q*wUOuK+yaw`gomBL?xe&<6)&^hctw#HJ|3<7;6a^U zsg+5dQB><~Gcn^A1Kg97M&A4LC?V&WKY(Ex7StW&2;1!~`Y^qD^9M9{Yww@0h=5l9 z6UeC>BX+{*ZZlwW>gwuS4d|>;uC~y zzlJfNEqg~Z>}-Y`&!H8#r42G9COJ7D7{kAd)7$mNA3l7rnX0Rm3!}*gT4dY(Nlc$a zw^O87x4e92-nDFZ3aDTLPF`)}9nABdl@F~=NC2arFSbS`OKNdy=T_j1-+ZYebURl8 zFC9per&O2D##NhXqEpZo`I14h{}=2%-i?n_CN79*caj+m==AP`)7=Is_&s=Y7mz{hPOM zvvmqYw^v3qfbx-a+vziBsAy>Jg*N|HJ;9UM#e*5l0TpO9iwjp&K8j#50e^3gZ*BD4 z|0kF4?Yh_K$D zhkHFl=!qOcLR!Gxhz&hQhlhf`drp2(Du~Lc=?v7dw{qluwgv;4JT7b3*UwML?#JIK zgaChWl9Bf!??HdcaWFYN>Q!juKZf&}iB;Ak#VS-cZm=pGG%_WdpLxk*Rv+WfFav;A zq<%k44GRlsvnVm#BpeZUmx0!l-7;11o_; zY`C5sjVL2_Qw=>gKfeh5Ovk|B6A3XW7{#r=y}7V7T#y1R$;Q_^V6U+Rd^9yx)sUOq z`dfhkoIE^3fI4a8;|9W)L-^=|zI}TLQCbAKLNcXTt-BJ0qSB$V6#Cw#NHl`J5wh+} zXL{o~ULg_lu@n`42I4kdsGp~ZKxhL|o?l+(RIuqwf0rH*@QXybUcGuH?mY82T;*gQ zb;1+c>@q#j#E1;*uk*{cqq$-xs3ayPwz<2Dz0w?7yEhZ+4eFBjamk%B>;9lCfi!2z z{2YG*IcjTcd{ko5)%Sr#&+ZBw0WaW~`%YgWbVkFy^-vjmKN)DxAT-t0Q_F0JIquw%gVIA_dzyNtmt$0Q zHr}R_sh)RAIQ^fnF8}q12LEk_7yoY-`u~J+&Hu4uy|e#)=THU`&|^cJzrC*~?+Da- zNpE=n{!6G*HcCn)@D_D&CvtCG0azAyvnl{Z&JK18SjK~3W5jY3m594c&*y&z&G0OG zX`gYZNc)qLlAM zeQ$ap+SQWzSG>iO_H~63rVv# zu3lwUP*hPF_F3m1=uWBzOGV>ta4^a92c*>o^wBgj${!y$%ay!h_R|~OmR}JPYu~|< z29w=nb7MnIRyG4Ta>3h6)YjWeLcl8!EfI{-V39n2On(+ELD74d< zMeREwe5#C$;%?vlc8?TiTOxH(JV{{JXTqq3r`2EDyWH=yNJ*Q$C{C&gSey=)NrAn@Fx%n4!m#5+1a8~Owq_|kIi z?#-o=9%&6Dqhq!nb|6vuqyBn%XE(REwK`(Csjgi4T_CcXfJsk(JTfwpYcRl0A{j5M zmX3C**?IEOtcy+W{(=*H2!c_~#icl`a`Wl1Y0*8v&@{Otbu(NT$q7>EOP2BxwC*d= zt-Ot^9|YU!1D~9=HL=Wh|6-mo)<`&UH34^(jxOf|i~Q2pF~0v5I&pt&88n%-@f_7IAmim zj^DhvuV0|lkGOM5Ei#fR6WlG5k4#H@o>$DC4>%Wec;&UVTZ&Jg_JeM4$9Kj*#T|h$ zZQT|{3GOqS0fzc`v@;>Dz<&U`Z?sSFb3a7i!)%LOtK z!ovpR$1u{iH8PckYzu0Q@?1N>nf?w&KZ$2BKMDd0=Cien#pc3J4L5(JB<}od2pr6K zO6|AVb{7s#11t`3ygb^B1!h~O(hhw&nH~NNs4c!o7eU3TZ8cJC{yr>hfMg&;j=@;d z@W|1Dl5%@>OuhpZkbijS3m8X29O~l<@P8rt-*dDuTG}3p&CSiD6&sxHYYc{~UXzus zsN#|mEm#u_R6$}WiA=clYjxRT{g1Zx7dp(#^yi&KOWhXHBuD`J3?szb*48HOFR7A| zVXlw$4?H|P1Wn@G)qi{(KJe<`kkI3P=$Q=E7PzcSn=xgYd}LI4c{z|)TquggY~U{B zyrRMg`r}7Gn~AD2@7=XLun4X9H|IxdiDM~#{;I0~aNP`zV8ZSh`nyhJA3l7@s`oK& zul{XHWp#B&e}9P_eu^e-WrYFK_+-T_Ffh=(NcUsP$$HuepeFz&2P>;)->c;Wlz?UT zFQ9mvo10a5>#^0ho4i$%zQ}b}v^h7+ykNiP?)e6LiF-f26rXJT#S~DJTLyZhL z%c0g9P2^}eozTJXmjB*5HA#`f9tH}GjREUkQFEc$Rj1%=mRCkxb+&bb8<|P|+Fv}x z^}7gBn7b}wq_>^%owQKzMjL!!TgvR6K=6*|%4%Ry)W_9@g{lq?H)S7}9`A-ubSZP| z3!(;X@`h>*@*symIgtiQ_^w1_=W5@>jk|PzKYuBXT}j@Q42K7X#iq+@K5TU@LT_Xgwm%Cli=2;H*3OJvG-dJ0DC z=)7mx-jJ&-unTr15?N+9qMoN;R<$B&ZfR)?ek+OTCkR@n8r9D5cy$(-+oH z(TTfd!9;0$0%?ts%!hda47uUKC^Q-kh5zwmsZRNXN1}eI6*Khn@LN)fzzf77%i7!8Y-ZpC5W=N^G zy^f~I~?OiUIu88y!~9si3$kk?-*7)LRMg};jz=l3XM&07Cy7_CMYKt=KBIm(Evf^N;$!oRk^K^E~i|Kao3{|52+vS9!K literal 0 HcmV?d00001 diff --git a/screenshots/phosh-overview.png b/screenshots/phosh-overview.png new file mode 100644 index 0000000000000000000000000000000000000000..17425942f61684c595d92bb1e8ead75a43ac17c0 GIT binary patch literal 104279 zcmX6_1yqzD(kV!%G$NftcPSy=-QCjNBOnsepmaCVNDC<4-Q9Kf{CAzjI&0#q zz2A7^9V1ngq+g-FMumfednGF)p#}%{!W0hfB?2-ccqhWqKpp&#!bC<*5f09q4i4^f z2prrUc^q9oD| zIteNfnyuU5G&mSeRzmE9=iEW2hcD^hza7~E?{sSSsyCK=j=2%hi6&o)KxAbo1G(fr zCrD8woIqqKg(y)RC@39d=qW=)Kda!fk8sV;%n*6#*zegD*u8TJ2pB%H{#d4WXstQn zcY`e7BH(en&~~5sZad4RoJhpGkjs_uQS$pL0b=vGXoPNN0*L_3gpjM0-UbFp94=z?9M z6iFoUA$+r}VO9M}`?P#vHd)6;vyuvgXKLL zrXWPBs!B=_(~Ewo$+DX&s;3T`c^Ry!_G@vv2YWs{%ZppxT33vcSQ;AYvS7Ua=4I|Y6T`T1?u_VaIczP$v9uuL z%v|XqQoMJLIPXj&C@cS1Cflbovl*4}nIujbM(o6g+3m`ZB+$$%K}hQ3pzw_9d}N~i z^{;H_Z(G>x@;@eY-X?{bg+$o(^?LF~S4S!)qffl;)|&Z)$-}3LD2+TxIT&f2hL_t$ z&e$N<&wX(76@BuS5}_WeRzi|bVr^T16k7&COqWZCtgC)WM793aLGkyb4+4+)n_47r zuP6S!Q9TkTu_82eY&XWHlE{xNlORTujYeN@=uVqZvKPc*ZG02T{9df~vrKL52gzBb z9q9mx*}d&1bd(TLRI#7qmU#S~7$%&gvRG#G;tXQYWWo=D3RoXxBtJ~j4vzM&YUO5N zi-paU)kiW?&-{)W$LfKs(JMrwW6L`3UVKI1IH*XtlviolmHxqP&(F#(9#qWGfFhQR zV`gmpy6gMw6;37k0kmwPm|^~RlymMr<(ph)Qq_7N2Wpcb^ik6&LrE4h+ClDdsStBw zx&>v`qlyGKg3S+GTBWa;mZYp7BP8Z!E_K5Y{(Q;%jKyWe{~`p(48h3tQ+_!2{9jDf ze+d(!bDyG)&}6+RONQKDUh7Z37$78=h4M=kyl^q@*l%(WL4!x`}|M>@(g)m!0+^|J}d z=&Ve3?Q~&RxBHpR8PF8ej2BsPQ|)ro#4(_8l|7Qx9b=PLag(a{)qnryZz_?X$NDnI zN}?;V@jpu=+$8LU#tf}Z%F&AF2bZWQ)Z0O~hH8`ha*Q!qiV(>>bhQwq_;E2hY`Pg) z=zRB@>4aL!OcvZA64kOR1lQw4e#J+|G(3#$9OF3v+Mh*A#;U|m(MpGSzW&Q zcIR7*0cMZ!{+OSM3C#wSKS8a?`AST#<)Q!8mr;jIB2s-tGs|Vzk1M3Vc*#jIAZ$QJ zpH++6Fh8;+bU$0~=JUWu@~tvvrB76PEd7fu5r29)HJpwd^JG{3p?|}-ff(({6HlRq z`~H^3TeOCnrve^+!EA=XUe6Qcmn@R1O0_ELu#jhrbgoTwjc^_|R&FL2kK7?;52SMi zYf5s#Jcxf@=yb(qU+8%@gWW-|9ILIybG0xU>ct+*asSPdi2rEL7zP;J>9Fia-bo(V43pKat0*XJHuc zB7&GJLd_r~DCF7`c7|x5OFlt;Of@-3 z4wlE$SiIK-xw7j%mTQhGgWv{A_wPMi*#Q-Z&mplF#;hF0pZiHE{yPq60*<0wR4xil zRmR*Xweyx956Awajpa|99@M%PY~M{<8J)B38WH))_HNmZww7Q0wDC)lyHlra?d8_z z7*D4;-`EA3cH$&^A9hP`x9UC&XU84wS(D2#TKz1ySog!MIc*R4T>Gc;G?M-H6OkrA zu1wO?#P6%&U;ed$j!7!k)aKsIrVaD4bhzYym*D> z_3c9(WEJ6Ex$so1j;NGG0{-al%oTgTZA;2Un$0?SL={@yoSic;3|l<( zR89SxI$ipgdGlB%8ym>B9A0iy%jMDPcHiOiG(cZ|!_eTrEhRS?b^BQHJh3^0%ss*x z(IcMam?O=yEUZ^-OsLEMsi{Oxi@9&+E0OG{;y>;AwszMOm_@fn48vX83pS8$MAW=o zQj*A4?B0;a7p=6RaDrZonWWxsE)OS|Pv=E3`hsYwWAaY}obZ>5GlhS*8?v9PTo}J{ zvOakf7ku%1_Ijf@{nd33b>Y?vE=HFpwK>ln zWymj0)xk)ffT%k?q?zYs9?(Z(KMjU{Le^AHsmAgg9Qsv{^7lQB@M>#-KV^BBUNd0=SdCFp2lVza^#C?QOjC z0-XqR3s!=C{;SYWM5&c|`|P7e|Ebe+tVhWcdmQafwY5iTikugCCNKS= z{_1wQ8N+{x(27r;kP;d&CcJci%U0sQ+G~H$?>zEb;dUmRFS>A-~Gn=<2xI%O|wz`OU8NOE?uR zm4vCPDy_{LHs1KZHgU&izN$`?9*=uI%=+yKGQ1Eyy_e=R5Pf~J6sya(eQ?P|N~RAz zPHEW5cE_##iQUp-K|_bRzNUPvsbW7nV!`gEe@o}9Jf^7G9yJ_p{KKrvnXUP+?H{dN zE^kb%~??Sav39-d_YFs$&gxy*R4tSPBYJ`(yL#ev~VxIRD;o-@n-j^H?`V zcRcyhP~zu-nMW|{sxM$>tM)i9#LA^tRMt$!M4IF2JiSg`_Tsg>fK=AdwjwDO4Hncs zLT8Y>y(%xw>vB_p_nA4y*P!88;P4ES<1~2tPqhvC$njyJIM&_5hZBNyCZ4hbEwyG|qmp2PA{WAVNA~6UY z@tW)U(VEGpQn|KOklS7uL_$AV2k8QkDJCZT_wV}>(xpJvJn6H7Z{_QTo0N142RlV~ zGeyEZYnx~#!k@ZX4DJwhcpT_SQPouvmWyT-Iy%091+RBl6jL{OXWtMVE&Slm`gC#| zlg&lYFs}%b&B0-&%g^IE%VTM&FGX?Q(&m+lu3Cs$x}PUu`0>&e?x*xe-vf2>wQnM= z_?lDX^=BcQq#y~|49?%Xx3=u39nQ!+NK_W&!||}@&=b-Twe8Tcs$NN>n`qdp+0ken zWZox6t>?IisM(kyV`aW9`?cqwKXeqHrN}6C``GbJ@O*c1=y%5WEs!FM$|7e(9lE?) z^Vju_hgi#wBzz83*>1R+Pxqv#9C2v0-@x=bneCd3LREhDKW;p0$|&~lnDIxxur^VM+e^1 zpWc0$K3y7nO7#eEC0zCHzckv|c6oU0$n{(zl}lyLbqk_d;* z9+AI#sidDUG$eg8*8eOaDaofSbW7guKKX_*02zX8|8$pXdD(EbEh0QHV&GxVln-rO zU{&I?dk=>xbi&}!Rv$Szs^48poV?yIa=n3eu+-GhfAur|pNij9lci@H+0n!9VTN<_ zq`8SWZtSHZUMhjhpw&<9c5BAvr#sA>>%6f_oBpi)Fjp|aNWd0B1?juAJWAf?L0hqSivA~9PL+vV%_i0?rXQXLz*D^6U)iKr?7ag zI=LiI3Qk4X;J-cc> z;JDy$<%Pz>1HCM&^HfFV!R|KU4G~{8{AWgWx+@!oF+m>|i>$U3HeHW*lD(t3Ay6Fo zxf@+p)V%n=(r?Zz+8w~hE!_SO?LuGLP@So=TklLVobRlE{ItGZYA5eatMXEk482$2_ncy zpU~68ZY6d&mHHIrJdKdF@z@y|$*iP%bb@cjNI}D;Kp7#y(yMT?B$8z^Xw?sRu4NEz z#}zSw-yVDj-2nM}Dq(WFN#lO%F$)_}IOMgUgvd58Li@5~wA3zk27@Yu&vQIWQgi&@ z?3;pxuAdWA^iVwN();q|S#u2sO{GX1f*w$CXYTtuRGzmGMX`QFdZBaWANYheNZdhl zIJ_@p%tx}soSanW!YcFg(<+|FULjN7Y*?^vM*U<(GW@qd{BWl%V)BshG$ozKqCwVy zu88~aIQG0jV(@4$7t~}iExfdJf-&uPHY9kzaSj*r`dt!r_|Kb8ux^ARclgJxZLb34 zo&Vr-^eK}LEp|L&(`WdShQGx$(H;#MIQP2!7yleCFXAOTvFe9|Y&by^98y5cQk~@c z!;N`2SEToIbD=E#pYRZW*V2GQUjv+>#EJNG;sY+yQo0of7iKfXZUtlO_Xol=+vqKe z%59L%Xp*&aC1kPH8B|oM)c)-nXi{c;o9L!INi{Fks05nvxN!^0{HJV-z6)i)kTt${ z#mg5MAKBUwEkksIrX1#iX6MK#DY-gBqL;=twnpvIe@xr0d zB=s@D$clwKuTMJjGq+3jwm*+GJBXBpu1K?6H);;%s>1(^-aeb|Ki+nTe~vqTKEQC* zTYvI-?y+Edrh0W5%a}QqDe{QuLO5)`zva<%W|G|>!^*pK`RIcM-|l%{r+eNzMzdIs z5TwQR8;jDa&*96RLWA2E_2eoov7D!=@*Rf35|ke}W}X|P%hfiI1aJ^!<~cp${@1bn z6|FCySKG^;WrntzzWGNz%h<(Nl+3rMx-B;nel=c`y^DyfjwYBF?-8zV<68}?`Rey* z0rIi>^yjjal!B-s9-eBmEnhQRN83}V)oEXy!P>10+uEf~yXa*7p~CHrbehLdtWbRR zRMHPWr(rT5K{Bb>AEL;eA(88M6Xw}7CtGy?vL2q-L_{8z#$L?SI=!<|ehO%9yM6O? z(U0Wy@TcexS1hK%)r+T-c!SgX@FTN{DF6+*T`n9y${jy;%CqGgoAQMXt!zvxnO?PO zKMI4EU$&i%DLm?y-5f|A9@j_yyd4~0<#Fxrs<(ac?BP9l7+z_aBo_3DxA}x>-JdHV z8(~B*&sV^Wqmqk`EH?5)?Icf3fx{v}F zrHxdPN5zKt<;MUK)2)Nu4{u7gw%8VyDz1@DCYdw899$H9qF3hMGZ4MoGx&9K$tZG` z`TTO$pYLoWttUbhnFl8j_bpw)d8de*-=0U~$8SsP%hOB0o358$V~U`0ah)IAw2QB2 zwQrKSQr2q+V}(4}kuEm24auipy|MO)y{OWr9*f3;-*rmT2r19GKf{tK6~*F;-Oav3 zc4_ydHMb!lv2T9)ob}i!ADx*i6+6->AB7EBVP)*B=j!WlV3~*+ulC_7?0BVVB%Cbl z$^k)!)YWNqQa~oh1+#X1_$Pie30W>d5{yiHoB}Scz6jM*$XD8tC{8B0-^UR`B0L|% zXO=ndV&kO|XpCwfwrAa7e|lt?l3jwAQ!WqNC4?CtY!Ww?To)amE1o3$#e~%h71wf* z$Tn93rwC5(x*jB5W-ph>+An5xiWjoidK4*QfV8EGtn|Z8BnqdF-j0U+a$FnTMGP8l zr%FZ1d$#1imC*`4KfbX|EtX!Tt5Ku6fj~3dw2C%-w_s7C_x#!+ z;F50b~eVIdva&AIq+8egnM)*Tx_i4Tts@$6n_5@h$&$+dNLPCF|`XTDw^ z$ap29m&aDSrBA&D{YK{m2)L8j=~z+tn@wgf#i2TncD~PX*@XoTLRWm=0cUqi4o;Jo z&*CQH>T`2(JuGIjfZ58z{b>-qQ|kS!R_%-{lge;iME-*TTkyOipxcoo=9|D@j!*d# zFAq{QmYxo&YyVytB)o7Jx#d(2ctK?+hS6SKVqo|FNMJb2;@`XV)?}|`A%o+~y1+2= z`p3gvlX^zGZ(DFSkHLsGy@SU5= zZH@GN{17p_0O{WH;C;C1Kp}O~g=$cU?mr#XPy~N`KT{u9Sm$%wa^GIq)~G^7{2tb%JN8`a4XTMO@lbiZ8plH;Qs58P(=Jq?mcOE3Y|4p*p>rN_ zT~aXC+npsawiI#34sDxtnmxy(&@)3kTABsfXt@qWh6|tX=`RJYiUjFibKg=a1QJLo zH&dt@b5P>u{HF29RkEe7|K>K$ZQYgFNjs3hFvy=i?)(Xg1piX(g}rb3P?N__149w3 z34a{LTh&ocl%LZ2R_eC%o-SHYBuka?N=7_N-%t4gxusthQLUHfEB|oevfKssq!*(| zbGC%4(=bF4ia3RW`U{bs`Y!6p$|+MSB+TlAKBI&6^hFr8o_43iuQs3a+5ViHC+9vcrE4 zi%TFN3#-|O?;^Ufi=?w%$Q*RPdKGB0If&F-kSMzauv5Y^7v59iw`)$06KdbR$6 zdGm@pBeQfqoA@FKT}cthzZjX4d{$xKm76+B=7O^@a!dbE)y8N>4%w0JQ&TL~&f1sy zM^q=jUL+529#(UIdW{o=&Y`LyYzBI^`P=nG^K551yL?sXWa1B@ok+75$uiRp95b3A z?p?WVDuW+S8#|{Qs;S?t#0QS8jTu$ra0tx=4jS2#9`_1Vq3Z4=t~i^nQ|&6b2NBLS z>l{hV{$>r@(d2n^B5O5G#bkvPO^bCOLC`{b>WLM;KQua6g7A24!f##ogo<2>P<8 z6%i-A`C*q~@b@+4KdEL3zN_m>31o%U9tNcFb6(zHG0|p}xpF*R!qL;i)88e{tQk{Z zC>aO7OYkM8F(8@p)`hlv3dAMMKI-nsH;K0GEJ#^HlC+Y&(&m0Ub@8S+r>&BDtwtDq zbHz?7X=F9N;1SkF*Aqje9emRdnSjt(GZvBg{rLk?%o-)Ie)}jB{+;fh8(DoWsha^p z9-3dbPa-B25rS#UJ{4cChI1{2-w$-F|3A8`FqHiTai`KpsH8L#4?e%c=F~E`+81nr zS9a*$z)}b_u)O(8MgC%!4yraK=|l1u*-Uiyzx{F=9Mzhs9bgaKk$j6kL0SC9gG_eB z4SQc&qw6dT$4T`c3i`REaKGCsT~bm3iOzRz7=*9KGJhPf8imMje zY{ueT5{1$d`;PsZ-UcB*x^n7%wONKG*g*@0^Shh}9adD7vsQXZtYkX#CRDvN&o81b zamlgKh?gff+Sl$+Ghz2^wrwshwC}Ud?{1|8$|~0)oJ}k`e_}f3GN>e<9)WcL%ciTu z=UTS2SL(S>8w4NA-!W;NRpOaCGMh136JuDYrij8NYY@{VnW-vCugNr1v4z#`Q*9{| z@MDKnP|RC~fVCd{O~%Q`7r=@TH#8>aKI(;fgNOvtx%d~#v3C!D>We%@;S!-zb~k{ z&kYZeIkvJxqOYmhY{PD;3?RN|j!I?qMk_N8+m&GfRyr zt!N?ae|r)=1%gG+sOkrJN-ZR&+&FymSaHfWrq+`RSndw$iLQds zcxp#;Dnw>1RO~jH66ca0Q9O0|Aj>Ski}GIVCX#Acr}WW;$_V9Lx{Q{BqH%chS6IYTgXif zH;&z&x6lr;G>85jZ|UF9XM z#cSFD)KDtMAPBmSfpej1P^k*Tof0#;lJ;+6F_daZjtnYF2=q$^c8sluXpV~OSQ>{( z<4$fgR_^`BYD4w`zw1SwlQ+>)y0s{;Dryrqp7@t`{^arPm)1Bo%@J}JKuZlevdVNC z%v*e%?rbS-()*(DnHU%tc1}FCwTV*={Q2^v<43QW&~20i4!O!Zp1fH*9-IdU2jOl8 zq=n%>m+$+YC1=Mx`}YxBI9~qMbeWK)?~ONT_vOm4R(oG(SdKfH*Zg0pcBy{b>-he6E01jl)kExx z$%pd|{Q+0U%VTcSdg0~!e%9iXTpH!NvOpFU&W^u5-@e{$JUeq%61x1LBzW3`K^Stx z4z6cqZ*RUakoZAUQ(a3d0tDbuf_sd)21dbYmla=@*J03RK zcA7`$>TGTg9Rh|5QuXD$y!gh1@0rPO2c%DKeQjr}LzVsZ)EB@mcnXr`Lrb-)f?`Q{ zCdS8qJ=~s`l$C9stai@|j~Mlq>j7tM>s!rYEExwj0#@Ul%Gg1YJgd>~B-*K|DaXB% zBCyO42&qG%MnqG8|K?b|rk0BN-ri2u&F_<-93Jz^sM!>~npz*m}~1 z<|nr@I5cE(zBK~7m3fDbO!9{nBskxjZt|n^+vY`k`;JHVr-yBkbFeJVvu3GI>#;m) z8X8gzcwL*$eue>*>8QGcyB= zY%aqYq#6k7IU>$ z2QyW{Lu?(S71Kj~u_S!A8_Bb6K0KX<3Gc=Plh@Xa?b|Lj;>d*34{tdSDw(6Vw#*eK zt90JJeS5P;*bz~_k4!o4mMIxca601PpWN3c71T+Qs^?Bz(ecC|X5=`^w|%nUGS)kP z-$O4PxO{AJw9q8@{BS-i+-B52{PkVaVg0x^=xazdS5IKay`}xL|;d?Uqh( zvHZAxF{x}Zl)?g5QwZ+n(I2)3x~zoJglutyYvdfp2(huTuoxL17Zn$OIj!gM27I)I zW^e;pcWMwtK zzdCHaUh-)L$&leXAsa(3k_F3{g#`tW?$3|M0ou*3j38e{Dd0ry>{uF>yqPrh-0;DQ zXU{Y^7^wNZLV#ZYg<=7$+rveXNCDt*yXUVEg9T51y{}Ee&Q*(vAIcEu#uu;zbY+qhYcPcsLD?qK34~wp8I7D zo)4$}UwNIVl!Y$wK&;b|Vz}&#Q@(!vy11;&tTzhZcBV3rS~_n0-#;lu#aApWETlr- zc#O0;DiQSAzDYx@Eis_ryk%ml5mFAL4R|`w_5zu=7Jvl*qTY5kX#P-2F4Aruwjm3S z-LGJ2QlegTR+If0Q)SYBR-)_|rNSVguCD&T?bILl=7R=67K}xv_fuiCfB*i4J*g0C zwVC$pp`q)3`FAYOIHQuMb@JyG@W-1d6SCOi;j%G+&Dy*}6%`%rbx_rx_TGj-y+~;j z^aj9c|4dCufW0R~Yiw>#cktWSVf_2G%I+I@PG_*URTg7e`F^wYb~ORd{#OUH-RjCh zy&)*Lo~xmRydb?e6q8wW>K!}(BRsk8c+PYgsVfG8gst@i zUOts|Q{LbXXH4)M{ne{ix=y|Lu&Nc&8%-$hID9Kg@ww0{tuWGvNGk>3!;@aP&35lBmYv-@Z*Y&XZXx3_0$Ya1g{Z~2F6 z$^YJtOVI=r)7Hy>I&gUS_#B`(E%{yRb*-|mH1dGr(_e2lj~w0&%0jr`{S`JcLK?r@ z+q$|sYdgcw+SR5g%Xj}8ynThiK7+@+Dk*z!%->7J_LvF9sjz#e_9-N!*C5))8@(yw zHs)m@94!1nxmaoFc(hx9XJufBcxEk#u~FsceY}{w1vPYT^Yn6m`h4Huxp@r!*tsb} zg35{TyH)zBTdE^(O!@vyeQcqXlid{8D^T*m+tI~^R-SG8b8~ZWps|ENa@@rf4bsWUFR#Fskq!*Tiko1poQCosHiAJ z*PO-{r{TJ&xVIQh*;-o2jE<6M4PeSd1D)-`Uu6GZ*^oPrH+^#>Ar2KI$Lam!&ErcQ zcBbi`27wQS`mfpZS~R;?ZpQJWT&V&LttA=pGgX2z5Ho}}hCL}gXa}QKLlCI`yy?e( z6i0DRuS`rUIS?ySL4QFrit#t<$!NxXZ@!IbCn3+&$Vl#0Dtm(3+kk3gPpnI7>HJ^y zESt?cNPVIL|H5BG2>KcvqncQ`N^;N?Bdd*PC;}6pQ~&;!wDkBJTBe_U%9r!NW!q{V zcVy(yKqnzJkucohugBSdsv0>Ei#D<1bz|!wV|`sZ^RUcA;CtDlOV~U$9@6|$75$~e zJ@@q>oa~zd0^%+wz2X_uUUtZ?V~1)`hL{2~Gk64}p!C&8X%uI~L>a1y57DHkp}EQo zVx#N?_0>uE-3XgCJ8W_ZNN~8gIt3q@!g#ICugPf#UW(cVDo0j9a&}&U7a@_MWwywn zkUwbPo5;jW!XeH;oQyhB4HEpUHFMZ*YR1BZ7p24?MkYE}W?G^liiavm2&KSeS0c)Y zYE)ss(*xi9B}0?)zZZ+&5%|!wW=*vLn(^>dmqLZ zCPDQvDZ&_w5-zoxd?E;(M_lSB*n;5TouDkaOeL@{F6bzydk%wkIRjq#HQz0Yk)vzD z4I))leIa5(d<7pF7?q)n1ipqfe@bJ~LV_xp?O5_(TWwAs2feDlY43_C%T56ySQ!~4 zk?LwFep3|G(prqI3`C0RLGe0dw~8ti8|MpWrWBN7n?WuHKlp_rI07FW5hc;Ygx3@c zI|0f26u#(BGwK`M@Qy=Q%lNRki(8wj8E|LAiz18ovH-2eS?9_{;0sqJsHRGFBDjFM zWhj9&vUBdqYA`rszbvt)C?W+; zc^g`ZKYJGN2O&xz=v_!Rt5r7e)N*Abb!bCO5oiaR;wDTa<5mwi%SzW3!1DFOZgW}% zh4QO7jGyehdrL`E1D3=I>Q_2IY<$`eUYjt8X)xgV(P_NYP9z48B4~Za0AMyeB|^vJp~EfcKqJMfc{kwT$oa#<%F2ZGc#*8@wqrj4V_+@9 z-oEeny2fEy7+|6Pr^ox*S}WW{8JeZ?^8H3#)}T(qdSxe-B&}6N*o`td#{sSc;JYXw zNB{lzpYKU9Il>BnGX*(0fuJ*UI;}|n=E2CqV%8Oc@+K}-jjCW{LY{3C0JBxly_p@_ zZ|SD>eOTUEjBZ3Rm9Jb*8;o69VX)?66R?6r#&N4iJxA}@4Y=4*CG$DpFfCI2PqXaP zf4=~3WMpF6KlP+UP?J-bG~Jsj@x7|a-sI`HeFeH`0Vw&1V2O%08 zqyPf1MbcAA^hf^z9L37U#^i9WuHUwsOL1?1ziBJohQD@lVxo&WUNGrUofKO9c+MGL zo#2wOdh=hJfCpznShgv<{dQ*g1(nsd{@(9rs7IC&p?Orw@G6}Ig>0BD6)jk@KmX#PJnD~{HlEbZ(-6j06=@fWk_ z%Y7w7264>=A85#nPju$ph5-M9%xeQ17G#P%i+F^eKwF0<6=iux-5<5Y|Bu)pWJn5T zkM#p$csm|0+|w0`)^xKL-J&(O(kSwwUVqui9a)v&Oca)4*xd&`OIYl%@JFVizyEoh8DZqIu|o zVZKNFSQXA+;V4m{+V4m3Ygmzk=dgv1GLGcSCd23vh@gVTMxt)^nT`Mv={QoOwVucr zGNIR$2ryKdC{4B+h(xvmIQ<$_U1=5Y4iUg^^$1Sk1K~UtoRD~yecwq+O1|mWL`FqJ z1CPuX`wQE6vwA>q5ORd{wLZa(t2G^o1Xtw!tngwCjF^`-8Uq1-vqDoEy|D;5FBiPtC;}3Mj=b^>uin1dAP_!U?eWRnu^M=`=x8)A3uIn z3vuxU$NezkPyxlksFv~xKaJB2)!=?^`oz2K4Ox8T0oV;Jh>D&bK)X;Vw9<8V!o+;` zjj(UpOC$`H4<96qj3|@o6uZ_>PXKpG12wMFxEJ+bx!w=}>Wo09TG32Vb3YZq^x$&C z6f%TeCCqzZKK;E-bA%HW$930fKY8|t7Eo?Y!J2Y$aT%u5**AQXXKRCkKdz_rr6{~u za*_6K<5frLf^!T~>rxlI&MJ8Y40nCM%a4#872j~oNiGxw_=CARnEn75UbEr>F98JE zRj;VEHRDg7vb(YswI)eqT7hD9dW=s^mamSvWonf?Z zDmZ~eR{Qu7KcE6gVdq=l9L#+@Ck9Pb!!#R{gq7gP7*8qVt=dAXypo$?Xz&o~o1-{Gx%nx=Yo+ZylHI z?TEunqZ2iFZ!_Jd+z7;f>)9aF!`*bp7L4k`$o;11%y z**BwrAw34X-28k5FyIU*)_;Pf3-2qdDU;+{#@j%yjO+vzV+EJXepCTro%=@)CLkP1eq!kdD*&jh@qBBzuW z41%N<2Lk>U#|b68fCH@Xxcwf?JCVA$g+M6l?H31g#KRjwcWOFn22|CK1QgKDYG$*; zLKL6Izn@2_V4#_P!_E?olh=O(=mT~0ErAlx)mG`YbG4BmwJYuBxiQED2MQIx0l`Eu zn*7rr0fIyO=o}HInrHykdz`dQTL`~y1MOL zG!_H^Rtz|5jjN-xc0r0y8eWKk<)JhjxR1jR#*|0yDyB(P04%D1+<0F0t*XWbDue4P59AHlcWf`kr=}nx8^kNzxI zqQnF)5n?+fst*)Tkn)g8PLWsW-77-|Pn^*NEOIt2y}iA@>mG9TvkbxzAjR>AO4 zspLXjiAZPu@u9&%`!RQV#P{_AEGcDiag-NF9%H}g6m&kd_yN4j>3*OUN6H6*Nu%7h z5hAInsr^Z`^2I-YcJE(S12M?j+8RhG89*_E@dQR%0EqDL@G9L8KbeoD_a;(H!(^aL z0YXMv08uHhkg-rh6V%iU49431?!m-7@jG*N`vqQrkQ9OTU1;SDgf3W9x;k2v2U4Gx zmshEFO&G{^)sG(~8Ssps9&TkAzY6>D1FxcHlpk%9YZf35Aa{`h909XB^rqM0^>lTI zONz1vgZ@J+{_%r~6r;wl6JCjL6Az>+0KBIz25sK&``>twI<#F9fEy0!HcDYL7~T8( z6D1TfH9egS^yQ1)Nieo=HCYHQ{NJyICYMw1HXwGYtEzT_4FD3XIlx&N{&%~lr`(Fk z0BpS+2gn1!%qrOEP#U-0YqI!oSpU}1`Bp#XQ_axLq-&Ka&Y{X3`U?-9k!7i{JUbS{ z0ZMV5%7t7lYKZ(&(efqL00de1aaXy<|Ir%;ZogV&{f5=A`b%^jV%kB=(Jg;3ogf%tQ=r8ZWo|EqHBbZftMA^Exvf<=He&ZEAU%m z*<;|WKq2qlbpD^n4>~M$$O@9k%?l8gFtKh@Ngx(jO<&Mh8$V-HOJQMz!+=!7qC3gv z=4Q+rffShj2o&%*nEP=)CX)Rh8Uff*EubQYhK2&518!shRKU1GD_#HFO;SGRkmhE7 zf8daCnhz5JksClHGnhK>K5I7h@88+iG2bxI3i>7g*ZcqvvrO*;#qj1AlJGCS?TlbE zZC@Zon{SV1f^(l{9{Yxa--l7q*nl3iG!BXyfSy?Rb5Kb14{bBx82-brC{UK?LYU+5YLrzS{!8im0)j<@5&|;XG$mR0W!i zKVa(YKoti4iWNM`^g2-saKAv90hEfUEapimDo6F!zkQj3+!=#?{5J(q={Z4;?li}Z zZRge3rvN5cVv;Q}ExhPHZzl+hDrSTB42OXGd8dHN{`H)al1QJUCVE!ys`g?nM$ld0{(mK3{JKxaZAjp_TExyYIKR zsXu@AT5A*dcLbb7INg8$ff{NB+7rgOzVo~D&O{dFRdZ-64) zzkdl2GeXqWabc!S_R~et`r1iVyk86oE**f%DQ2*>b3d5r%$1C0U}Nh)bibCD0ZT^o z-Wo6aZmJ0U8qNp^#>~2n3E+o}fNtv^_)v+6WG5xz0H6fh#}{kHKEN+=1`gXhmn{Vt zQYuu+90W!KD5U7XH(IQp1|o!xLtAew&(Z))5Sb{DhdEz=*Y9;wdVwo50`!I+K*xja z=3$}7{HZpA#Gy42R!D$B{T0|Ol|a8)0`cYovoF=ul5R2eIT&EFW)dQKbzt05@H5fG zzBo7fOTB6B2KzU5c1qtsTGL`7t7vP>GT>1GTxm5)2SRa7=$aHj9afmzi-o*$>dEVS z!+yOSAOakmQk@2jp=u!1lL~q!h=*hR`S-66*tV8dR=X!lfIVdjcuat`1Wf@P-B)i1 zM3&ihKLOyy!dzZB7>|O5G$`Ew&;h(f%I_NS@1KVI(Sj`SLskxk8o*Nkw-ZJN0Vkj! zLO+`^iC5Lu{+%V{qXllexg3@~pdQY)dc6bKwg*_Wz)6mKBd})dFwYIl3Yc@ImC|cd z(IzD?j}|EUSp<}3aADl}`Vz2HfUoHl1>3p6y!z`!kM-;O63`WZ2M=l(ND8l$PNXX4 z=!Ru~5|D=AF1)tBTJSoqz<~|~cx5Gk*^MG1dZxqS8J{CZlv?QWjya~VCySKHO|PYz zf}uFW@Dd{STnO;-j@NGoQesx56)!q}0l%knszkl%fA01dj8g1LMglKlzxeMlCm~Jm zVRDITw6``L2?}M@Fh$79qQ|lsD1Wm@QN{^C{~1E&sw3c(NKiWi=-sS^BuXsi)6@{> zpP+9)HVOef3(8Q;2)fv#2EY|V?p1Y^3jR{A!dK2v5#Z&6GT`C)a5luY4G0F(3MjoV zfybp-@(3du-2{^?>0`APfmEC|*)!J+XJmj@QpyC(|X@20QBVZ^2fsF*o&ITCaM=7~2A)+PEHV1mjexX=5G)4MW9}~Z z*WGAGdDqba>l*+O>ba39T`jZ_MH%L-ZJL;~Qx5C(n)+U}z-R|B;QT!MnRLGXD|nM< z!SN;dp-uBH9q{V=0N%M-*cR?5w#azFOW;U@)46`?2&19^tbi#IFwUVEeht%b0P#0! zeZ9!jej0n@0zrm#Zs5I>VvzFw`lzYb3&h740C7Q6Qq*9Y)&H~yOxSn`tb(_=AFknM z-yo#wI->xD1RIwa{>B41_wC$tyAyK?2qc-1Ujg_ptB4`U}ip2mBn4>;3yv7%GJcb`KA}K$?Pq zh>rWkW7q`7#7LWBrmCTVD5|Qe+G?C$XmuJS&6Pa zjOHC&lF}Q2P0ax20>-k00O|q1*s+8KJ$REy$eT;2fxiv|e544{5wyg-y1Fl|t%6|W z<@eYa(8{X3&cVQlDFC9N2g96d-~^-r(;g5)N>K70S0SzXY+#5ZMyE5q@bss_V-!r^ zy1Vm+jRlE|i?>ER{VUS}lw~-B4>pB_8z}*Eg~8;FAsDa$up0>CusINL>F#P~7+3{G z6BwqT@W6T<=+nTe2r%I|pUh83*}-pge&oIxPZx z!8jY8fgnXgbB;wu3F__6!NgAp2s2v`$s8Fl2c~Vv%e-TjoTej@Lu89S()g29y1&G) zltw#8g$9&Lw4TSlZpvX$6fb}q^fHE`K0FJ&aFt-}hRb{C2VNmP%wL4JlflPTW5C-L zM>&JblRPw`9Rwe}11=?zUjzXj_~cQ#q#G{xe@F0uBPa$x=BO}u!@i#Ts`Cv57?pP_ zC6g~BqD06%o9CYxaUxxH^wo0)xWPHF6t(;M^J{DICb_^!((dVd(4F(k%R%GxwzIk1 z`>X5r_oE)!^D1TlK>u zDfyp%1tIw7HtcYb5sFJoH!MW%i9vHFOM9V#E;}W30Y`}?-s~=^ukTZ)7vJBV+I~R` z5I<`wQ;EC6Bp4C9xJyq-N%3zX1(5j$$TP!{^jR}S)2UFXBghnX5R6#?PhKEKqyat! z{B5nQVaXUMq6@B*ik9hja*S%y+Q14Xs7U}LxTz^A&fusM*scN^Qv)<%*jxgbWcv+H zI{=f>sS;(>?7&#of;|tIzkqQOK#J8hHKlcQbUN;B(&=C*4g4A;MXZFcSuwH3PM+225*|`eHFc?1&lAj2$J1=J%`)ghxVIZHaeC;*aU+K`90Cd$OvG^jzA%Vja>mopYBS|GGq*< zPMosO!5A0jFVNgyz;HRA-S7t*!&-ka>bBI_DiNn~bc%cjWQn99%76EF69-YRXpPw)M)u4!=@ePqsBpA5*=Kod!F8M+( z_bmUzr*a>5Yr-x9$>2^$@Ow!cK{8(ZAO}~AxhZi%otbr;!u|YE<+Rww)N7$dj)@-JlGo_Vtik8scV4DMACKsb6y(>I^j&D zlZa=A|JKC)0b!n_i^~QaEV|~3ae>C`oSr)XIXA*xxXh|8=thU)UX-i3sa0t(q9Fu+m4^tr5i~Tom?S}5v%m8di4qt5X{(46m9CE%)<*F3V|$qG21 zsx-sk#*x*MBt*nr&_{tF=pXHH(LDkscvN6CAU2D3S~1i!(3@+?o@__$&{J8sKZZJd z_mZ=F(wxvY5|n2@)k#8l{0Nc)bOZ%!?BZ)}Ha;&bvES?%gtUd(?;M8(prK5{ITUZMgxPRMk50r|m%>59z#C8w zTGat)Ws}OSStj{eA}B7ex2d06m|Hf?`&d2B0Y?&<(;DBa>rf@*NlKC-?SKhyrlMmsF!@5xvdLD4 zuS#YGzA%T4@jfIG+*U(_@bp&qQ)1lU#nmJ4+`KV_jZ_<&JKJDi&9QwN54Ay%Bzblz%ah@QuJXY9KL(_ispqm zlvbl}8^y9cPc626nSf#)}GFxGdyB3@9qDEzr~@VYue1*@!KB1-EzG#jBGQpsB1W0Njdz}b$>NKD8900F!14U%D0PU z2on>)@yC+c54QV3EYPM}@>MqfbSe zKDo(t1CunQHcL4B`-be7kus-{Hd*t#x^fQ2^V@FlcOgAM--7EcSd984s?9&GEHh8> zTG8gEX?5)F>ginV;T{9KYxbUk!>7!%3I`9Bndm_(9HOW=?fhqmEmqQ8jJ+9U>@nRYb zvg$9!TlqhP(C611fv*Mlo0Ud=W>HCqOoMKejTL;VEswqt0Z)FU$!n3u|2ih5Ut(Vo zRM1Gvnv8;cDj^sP{o4T)7YDoX^c_o_fu6~TmTKiq93nI+l%!*F#*mK3T!vdHez!$TT92p%#|Noe;AIF1yYs zhD(Dfy$?}3MKn07rAbMu@5Fxx$qD@8ZA1P8`e!Gc1k++p?ndTW8>B#U1N^n)bvI`l za>{+{XFHmf3Ho;0kPb9zDcc#}w;2B&U^V>QIkO^`%Im1I+)gFRu}d|Q(^fT2=BeQ0 zd^=-PSRAqgkyM*QCwTtKK%-^$(Y#DDZsC(z+&~fP%yocH& z4==AZ2om^>>q9j-P>{8my!JvJ~cjXlUTk0|fOC?BLh zi$%mGsj91xk5}td;P;`*2W#o;z3Jk4m>Y|+njVZ5Ln{`r>ICQ*vi=vzL88+XaAX7M zXaJzX((q}IfPRKzw>JQ`rDbJNQ2N7wxH+es6fbHXsZ>*cT0y zb8>Pf(uiP=dKH_!xi19#!t+rifD90FLC&87K^|G0sz>JvWZIxTKtYl>{9hjD31~H3 zE+~qJpTKG)EL2Am7XX=MoYmQI;aa*t?HUy!HI)^mNyid4)z z4Yda(Y{)w5B4_>oMfgvPM!HXS{ncx?5QOvG3rQ-JX*E^|JCyb^xfN%4%{_6I{YjUb zn7(a(**Uk*+MxbSCxO+}P4=y{tocn_^7l0VYsXE`K<*XW%$w@oh3a{6>0V)Jx5!1y z#q&I>7^~cgEz;^;25+c=+xXsz7vSdK2k9R|a03`y86>FyI0L*LQM>25)S3jLF9xKn z+2T%Q7{HXF3(J6r5n9ag^7BIm?+xd=$$MKDmS>{d67cX$7IWFc)|gBw*U$?Eeb4}- zK@f+(ESm(nx1FhtpQ(wW#x%aWd#vDTEP6d=L)*( z0@*Q^;@M7Mv)dCKxlMJN`{EB-bCzPnRU)hW*01L(?o3jZ(qa~&%j)-oj*w`r&8=Iv zQZwW9MXAr@sPTleHoh_|DW+kOF7Ctrz^hM6_wRoS4e8ds z-mF8eWNH`d5maLF<=@!VjM3t^1R~FohKA2^3+LCtThsHAj78G%*^ogHnUQw*(4{@%TlAG2l;(Mt!a z?~br^y1NyE^`Q;61+`~%Y6YBhqvwViK!6CG2sjl-P#B+_oD79@5?%po5I}b(SZV#} z0xK#Q-YAj;{mszrYarj%lIYM)=#608QOkr$pZC#y?R@1VwF~&IQGv@h7F*>L0zYW8 z8Ok-7I>V=1ZN1M%SAc=jVp}80e2cT%ydh%doXUFh@qN~srGh?rFIhhhMXT*mco&=5 z!#+usfPD-o&6ad`2P-PvlR9@SUWs8M-$F>zVOls|`dQDwvo*# zgBZ#VR#?F(@lldBM?&{6_m~3En*rVndAt(MXaukRI394Nct{4^$p6eHT4^T^4G17! zg5Oc2=EX_x6yNCiFXUSxi;MSCjQbH<1e?X}y*odMvuh)~l^7nr#6=~pD{}L*nSDjWxxDL+{q+ZvRAqSos+cBqS`{&wG09;m( z1{{P{QItEXBc_Tjc3$)f$r?S?yE62IRCyk=uAEQ*F#7K&d7A9@%iGL%9>n5$1_vtu z=e07o$*5Lpd&nhruPg(oA?T4vyACOko$%uC4zNeJ@i-{UiP^CFAMSWu_K3m*9Zin4 zE#)875=djMb>5gYXN2c5q%{jn@zrF#<;dtMkBxXL{&zuPHOs6|e|jhi{%l~x`(UH^ zk2cv>I@NpI_}kN+Mw(jb5q6NA_GbLDs_Xo4`jaX2k@RPU&3Rlx%Bn{%CT!XfUX9z? z`+I@8A$Q81#3or2pFUtzdLE9+I{9VtjRxVSf32C)VI|gXUa5-xG=6h69RjU|g7VGZ zY?I6;2UDns;8XDr|N72>EJwC?+6LxCypb=g6nY6=u9q)g_SwQEZ*FaUuzC4A!620< z+#Nq?anh%#?1#yzLoi3n%em)sT%8)C?MRc)WF1YOn;2bY{u`^%L4~}s{diid^5sjb zzG%W_5I8Yj9a5&zXIy%UJjS_P9uP5L_WH}BgdG<5!uM>OZh2QvJy@_*K_lC-E zYpWy%WQPcBOwAKds2c$M!etl~47d_>E;|!KI-tOT=05;!{D33fH?LueC|RQWZT0uM zzl!Dd16iR-r?(Rtvz9mJANKkdm6V*mEutV*z6~%M^sZnj1Ra+!+b=ZQGh#nLBf@5F z$%a)%P;2dlZ3fCEM;mYMKfGvZSmo9$U|98JUny-YpIq7_UrIn0A0N*c$i<}ySaf8d zAEVF}0h;vrG202KUO+3U;jBHFNZS`dtS}7Ss-*w51JN=cG=coS@a9eSLX{5Tl44h) z&*e4QG#4O(nIkIZaxQeMlV?3?)p!bmo(8FkzB?gF6;318cVu?#i4qB2g zRq5V}bIRer-<9>+d%MwnMOIgO4I2_@cvdFTOC`e-={r2UgrOLleDiMr|7iR}vPH{j zCqn;V!C(G?iLG{j8Ag5}zsFp=+PdN%h+yr=&utjGAAD}FaQb@U2q>=pw&GiPCGsbG zXwk|C13i0aY@_8@@cCZAlY&|(&9=bh{YQ>6JE(l-1^pQ5`u(-aiS{&%B@wyDB2}A= z^!K;imE=kK%SCgH)F9{nX+`;|;2MTFcJzl>;qN9s)b|5ezXh)T;o;+Br@(7~ngo1* zbPWsYuh@z+;;@^XL7ij(svmHX@Rboz_fwQ+*n$$%HhdxAsP0?CA0QSnfHo-5Ydt0o z`5(V2l#dF`QsXa-*fq<1({8`$qth61p?CfCXijbJL{O7)1@|YkA09!a3tgy%4p!2` z6CeJ*wU75K?O{_v)2wJ|ab-wg)r+p zKa1}XCNMQK%a(94x*@WO79oB+_0QHo*U4eqGhr(HP`%Qvz!X@$qVC$@*D`ieI4f+9 zYPioapE+j9)O!B24^wBRvn7Lk+d}W{5JZp;bvUZUTBUVRO!S6Um#@hOK0(1PsDkZ> zlt!)M`#qSGb#mKS;18raknn}%;SGfYkcl)mymCHe+ z0uhU3hbJ7KRA=vZW~H!@wSs4*1|ETd{H?bgmL$msjm?ip!-_N&RmrJ1azcso_~yH3 zZ2II5*m47gW*5|#SH6iPbGBEWQ9>Q|9AtoL*~BVS+{qHF@k8U|FKMtGGwo#q9ehs< zGgR5uJ`}3kL^Hmf2CQWio-N6*Q3_clg5s({L!Z{P8;h4_^!)2IgZQ}yldDjhjran1 z@{-1_-ha}+Z+L3fzR?!8PK*M)S8efB#6P*zBaID+B0rE zY8u+U;yPopGwquX< zVa`b70vSK17<`i+kZg+csUCWedj-Sg9fiFUJvH(ylF zL|gl0%?q~&4L(s)c~N5hfCYTL)YqVU4YaP^pSyt^w5KZTDRZ2!?M&#+h-h7_kNYQH zcS?U&Rg%<6_$esmQ4&E*X}jVVG#vhtMJqDneF5g2rTdQ)=5t)^{Grr%nO(J&r_SBCe?(<@qw1N#_Ck=8x`LtML923sS-oRpZ}}6dB8_`(NY5+P zStdlJxUD!H*cMv#=_RlDL8tWY+~XrB*Xjk5y~tw-x%G+aIJt$ThQ`##?`hKag`m@o zCb8cIgKt-76s6m&Fg=`vyH$n#n|n0cNM8`9JU*kNlYP8cZTEb1tld-ha>M+sg)W!2 zz!n+bWZ!bT5A%>7p|uHJ84{UDXBbNot~nriufXb#QTb8S+t(s)Q@zT7Vi{0H50i7L zimRs{yriLTbjf>}f%qIg^V7-E>u1kfiA(|E`Bts|0U0WMf)S+Urs&5&)7Z!l?qaW# z*NFY|jZdy;pUT7tDUgcaf#yR5F|9@ zA7!J!D}S@Xq%*6cGd?lgFa~aJc zEyuCO7thMLH$j#%o;33$V}Y(&xl~5m*H=tZzwHVglUxd>OnCa%)Q5vf56b3sUbmiS z%!>2Yk(u@F)Z|<9w+bl&Ry;lqr00g1PF$Oy?R`{%-F_-m6FS=~Z%u;(52PO9#DIU^ z7NL~c1L8U$^q*cVXLA4gME{%PQzWur-_uryHi>h`ZDXw8F@nMWQF)T8rB?ug(p<*q zV^jBoF9X<*?i7qr8ep}%UdhBMk4zodeHvcPXr`xM!2CV*_)g-LY0RZDjwei`iDFs% z1zO!aY(JQZ>;BziGgl(8Bp;Y*6xiDeYf}kN?W3%> zCZeLdh-=Y;hHRGq=Z~{dC39JG&$yJarCda$U0s@D{&qzMS)cHF!&rQ;O6$$KESk|<=KMGw?}Tw546dtitF34#1rjyeA>&urKg=I zoyKcW4h;q%n~Bg{2()1|+t8|8GQ21fzp6o zUsz+L`w3#H-18lU@_iw;P-sF~YJdqq7O+XHX33tFsc%pdQT z-GEXP;Df);+c~u|Z$s1xnjB@w&MeR0-Gk1gwrOfYd_15;Yd}k`J^2>MJQ+Bf3)-uQ zr#C3b_N{2;RA|T6Nytidv@@cx3p>*&9YSJsgo2~kJI682hBC>F*4{uL(OFRU*{Y$T z;f>!rC6{@c@nQW4DbcSzeeHcGR>;piW>`YQD!5l@5H_vm{*F`sstE)RX`_4XpIZ!W zjpag=6G#0lMV0N1n(F^_0cQXNY#kfS1pGN@15QwH`Uw=`05}l_3X`A@>OS>L`PqEe zujdVPN#1n1Wk)>gx&95aB03A(36m_PD)WoW+upx%x$_NcGuSzUFEl4hRhvq9XD|s0 zN0w9Ml}9q<#hG|2{8l;Q-|dk+Zhn&VD@0-&b-fIOKJ&c@k3jhE@=xT7HnTro8CJ~$ zK#GpiV3&iuE(eM=r=}Cex8gpCKgjoI0^8AS=``T+<~y7W3I#tw%;kDbYejs<^24n( z8QItd+OT(RFKVE&A2L-O_|?||i`#U>D4OaN0@@x>vc;*gy)Rf{YW@phH{fRr8=ivC zt0LaEfdc`WK7i%Hu@VDVbQS;y=&~D7Y3Sw>x}g^4H#!aQ4@mvtALCBHGsR=6EAAYu zS6xxSS3~ej_wIQGy~&JJq+ug<(ANt;bhdx@{;kO4Y1v=hrⓈ2VUvj(%)%~PL-Sw z?|8@C^`pea39`2l}5()1jDQA9kK%8@VE$@GiKzYla{dWYN{ zJ1U1@pO916NO~Gt>J8z$?vaw*ShsP#ZjwqT_)8kq8F`gcodP~R3dg7yq`;hVtVI3Dm%NczV8_VhIE6y!0)d>SbTkHt8{u&HnzItRsQE z=xeBR1$N;;l4b4LHjIGZyA?7A%`?DNR)Fus#E?8xR?WdT0WDKnl2k?%0ApLax_aPe zV6=4?74N2+s-gYie_9MdJ|sCRpJfo!(Mv@OVV-}zWkElrZY~rvg?-xeZnFg)SCs3c zAGXa4=Z=DL4{*6dm7O)T*gu$~wqhpi#(YnUomy?f9WldAJD)MMq((KTP9NE8GjC&J zK3V)D1pFpsuVg7hz?Bujji@Q$7zgd-4G`^@B zX2{{{Kr}j5^<9o%I)>%A542al>h@P6%1!of=C-tZzfS(Lv$y{uuTZzgSZ3J#aqZIu zeRJz8rUB{64~tDKX04?kR}w^9uzFemM|mZ`A3f}MEWOP4V34HG_Eaf_w3Ksb?1 zR!2w?3`nZG4E=1Y=}OpESGV?ZFtgG~xb;M6O)?hb+oLHM&8hwT^@iF4=&B)GnVg=UuHiW~ z3RURl&i0^MQg(ZEd(w%)Zkzdvl2ZLxpC!}l#LE-gv`0#+L1tX^FvJMeGbwIcRm5tLULjK*E?k-SW(Rku#$M{#<3s(YsK3pV-V3|=s~?5!f36Q!m=otDAH|X5 zovr-t(dY*NdB2 z+ByY%-gEeu6UDIaF9otx+Tsu_od00kK$hBFaFRJK8Mj8!Hq3wA-zXKO?nuRDj$9FZ z?{~Z^D`mg&Rpdr6W@fTDV~A*G(xkSyd2$rLuX$@vD|RcVr-@{=Q?Z$~a36wC+ho@| z@}@JR7U2bs-)(h@wlKL^go_4iYK^QiHNy=CC|)@etSpq7a?ENZhm^;K40xH4{Y!o= z-}rD|nsqz-TTIdG^uO+LXL|pXF@8U;-uFrLp8439EE(GD=4!q1)mHxW?f^|smNaVY zt7fnybG5_ov-j@n4vsiXV-bSKk`S{t_0=!Om1;ZLULxKKwfNyQAGL{Rk`+juUR0q zbuJlggOV6&J83Jv3eEgZGEFXu)NN)^Qs-C$+d6vKUrN+^^Zxg}WM6*Y{RhMW?6K<) zRE=flux1+a%CzdAgoMs!8i`wYJ9ls(yw z|Avy#rU#eL1!5dXVmDb;B=4wQ_H~5S$b|C?jNz6ggc2;xlj5_JDM|Hd=XO0KdxL9A zkcwlBGcRfS2#d`|-8Qq=-BamypZL|>)2oX!ZGS}6?VDK?P@YC#!dvnZGoapSLbF!RF>vPXIojUUg~?hR&L8}>UWZxEGJJ8c1-jY-{0=5N?b{+zRlbH0-&;k)@_ zz>EI3h}uZ9$NtIXr5&EuJ5R$$FIwtsmJc!G9!8Ul3SzeZ(q`6kcQ5r&3+!ofGka4S zDONnjdq0%n(75 zdrjw;#hSh=mZ#fw5`Ei|Vu4YY3yJtz)*ci^XgU!k$@=c7jo`}X%(|DMP+Gp#4SaR|OTw_urMHkdC$ z@O+e)jrc6cW1$imkoYTWRD)yLR^HCzA~!f>;_p}%P2i28UvCL$aEq&1#ac(wH}ZB& zHvQewS?V{5K|SZqZ|Ztw zg2wAzvzYw&yuFQAb2x=VanIGydiuGuHWd;by{c_s@{;bAE9J2X4dUX}9YKiQEYjW* zGy43-0yZNuL3ufyp>HF0hu?ai6WSOxCVSsYGcf5U3pzjeYtAa3DA=iJrdq0d)=z(Q z{!qDYX+bGb-r>pBZ-2U@Cj;*@h|3CT2Uu7<>uejbH8DyV!Hlz6{-l;n@I%q~ollJ? zsaf-6=~HLZ#pFWy?pUqhRq53^QW!&P8K%UO6jZ80b!o^k* zQ*7=%Ig>k&L#daxns}v?t~X^!E=81U#wSUZZ+`U3dq|HT)ahZXj2KRluADD&%&pC0 zarsO(Z%u4eL%dSf@P@O8@7b9CsPVBQV*&@0PH73x>1F$xvU!F8PkG11S_wzKt-RXiPL>(ox}lKrXR40W+3ULCUhdG#E!86D^Uj8u&|Csy^%Yt2^(8KG?Qv*XNVvqB3=F zH3<~^3W!g-Mso6svUg3)_bedk5eRBhpUBhzRqPZhHALKGF*44MoAvP6_8OtGAt%Dg z>NWPiwz8be*r-}YIZ?Kck5)e3F$(@d19n4dDT^Z`$y%1nRPuBLado642Uj{=h6ICu z?)`avq{Tc?5`$|(H8x}!R=2BumT3`w+EvH;n?$nWWbI9QIk(983NN>)7)f&{cZWjl zmstUmXiWmQM3aKL&R37_*Rhffm(+I9h^?bq2}_-%Ssh6U*<`b1`-4;d#>OTyP4Nip z+^S*WG)ld(lq-X!SBN^tvkiOVAx0OR{U!fUw=lySk__{=PWcT<>J`<=8)~l2F)Rr1a#2;dh>~>BI12E1LG>P?e|+F4h(A9C{#oy zYGBfna=L0{M(NQ0J)IxZ$Fy-r&25yGW{Iib_gHiLIwi_*=F-om4 zOtrOHHfUhApldI~$D;1=X}wE5mCg88DZ@g~hJ{~~ms2tw;kQV=WJ6wJ8=dOW9r?t| z3{fh7hAs2U|KeXmDJgG>$Ox6XYASQ5zlVANQ=>!*oz`vBPtilH)gKVhh*Z7&Go^SX~os{|2>6FC1?7WN;4%# zZMJAU&Bf2&j-2?oGN`v>#<*vjCaI*5Nu$NIbJs0aNQQaaw@${8Eb6az9gs9O{g$MJI0DhLR|tc0gHl4Ewuwo!C{i8wOqH| zT5RJE%dpR4p^$zC>o5DJ3!J~d$*MIJqL+G-5r@B+KTnm@Rb)15472E*U69e;lH*EV zUFIZk$h%8{!zHOi+nZ1@4F=E;bF*CgqZuzp)cneVgASmLs2)l2Fc`J72$>xWR1e+h+>jKdrHxsJnRL6Jjw&#;(#e6GuBTKO0XW#AqPyTfnN@ zmj99N-uC>xJY$kZ{K1x}$dTL46>R%n>%YL0 z7aQ*flp3P{dn4_QD8}6WpkM-`6J7Q3u^qC z3<+ZGwC(o%D3Rvn076aFC0`SFDJAm!<2OrH;+A439A)X`EAmVWsdALzwwB=qzlQgS zS5^{YS+iT_0!u5%H+UIFjbw`nl6h>Gx+i@#RrKNedD`{`9(VR1Jfy& zrVqAC{7jQnwco3B^3()jNs*~|e`R%<5(?ed&1kY?9f}=@&H=E6W2B)mgS47xoWyJ~ z@?iMIr^U(`G53FO_YXW5n7laLTxtKz4QAf9-ah&}j6E){EApT73%IbxFL$Eeo$q*KpwE&T*9clKqH&j;aKs!mzpuj#`eKk>8t9n z$#-_TzZ4T4d*c7d5{9-@X@)$kRms&C-x~ChG7H{{l5!tD$$t6Fb@EU&aZh*qp?RB8 zBMeGTm25rWh>aAFkhG-DoRqaU^ZWD5Q@o=?fPA9Af6&8aycU0##ZxQy45eY7u~he? z;ooA4cko=villc>$s!ZZ+@d%pu6YZl9ed=#4UV2ZS%<@s$aICjczxeWAxscp*3cl$`N(Q^=<8YR?n76ws{1-Ks z)#NIlWP@KG5ciX*Ykb(;$7Exk?Mx4vdCM@!-^K4NK^(S|Fe9Q*#2)IMLK<;k8 zf32Cb1st+*Ho@a-`b)}b*SX3KeqWz*6HBc31YpH^m_q`mSe{GfW14sTwu=A#jqYEp zYU{zdac*C#F{LrT#Ci$(h-SWdKK@H~Rrbq={CKwQRO!E)Sv{mT4t{x~H>z$C&&uDVu9;&tz(7B9NFYs{|kP?)SHy<1v%*eFmBv#RH zO^vLtUzfY&aNr!mZnk8xXnzu%xKCP|>FrUEoz)3clq_g;D!44Qi zVasZ*F9#RYsBlpkeEd5VQ4>kMmleXfU zbVpq>q|~{pX?`1s<^-nxN1?GM6${B78guLS`GYDGmWmCiN|dL_xpc2~t+{aKR$3%Z zJ94&l3jS6KFRS)_Qd^=pB&tPLvrXj0fyMD&xjDhQb@Td4LZGf0ti(ukv zcfTs>aPl*PxY`8q#J4*t^SL|UE!8~_Q$6G{Pz+5xO%FLM9(oE*xsg`gC|{Lx{4G4s)FDxY2j%#g%7`lXg* zIph0Zi9DFOb?E;AnUUdBCYP{)RA#${Q}d<3Cg57Uw%wtxyZY$*IWG?LxFEYx*WFzY zwmhtEJ4a%RJ)uj{$lfyTKFP27BA5{D#C5NF)2A!MpY_T03!OAP-46rbNjGS$`IYr= zKAw(EUhuzUrRtbv*4JHXQ}vhH7;zGcWT-l(cagroAyssom!uQQa)p$G1Esod7D6;0m(i`@?^=0t z35O?|I~`+p^|9C~E}NsqTD$SN79(SuPa5|MMXGxDak<^0CPRpqt4__3&6#{^+PmYp zVty6{rSM2cvavH{mA8a}npNG)HGQ7nx10yf`J?AiBB#?>7#N`#nyN~7ftEgVDp8Xx zF45=z$=j6heNCrB=PA=IeTE2aH%gdf!&r~&ty)!K*v$J(p9c(MJ=Qz-koYIXQOdiv zi5Ah+dWE^M1KdNc1_xEo@L)k_57IbFIo^|LCOLk+hjZV_i5L6=hGaGX;sDYo+hj@HxW(#@ab&EAYL%WzDgf#| zAg8AX*Fr5Ar%+Q<155|nJ)kQ<1l$yut`RXWPcI&^UV06p&c zCil$|@J<^A77uMM^052-LE(}lK?ID+0Mh_aMuK}B&@T-K`>wvg*VCb06o8*Ee0B=s zN3vD$G!qb?(+)o*4;}IC;cHc_9+D-$a>2dXKs~Ullu^|jow1Sp5QLRf0;XtVmOjvc zJ_0+pSf?D&aDyv9^r}O^;1BI^g(l`f;7>8yn7rT)nvIG)FFeqM_X`t%q65u;f(b== z`Mzp6+DWLRqoexGTjio*eoUn}evR`0{7!k+&Xv=(yN=$HOWN~%*NX}~3FtKyOO2ki zG&g66jY~%;vh?_O|71k1)gH{ZjzJH#5Bw^6!PGKK>LC+&H{5uz1ag2iK>ERZj`xsl zQoIE#20ns8i4>*w%f&lR@H!>=R{xV9>q1|fA5}&@`{CZ^z4abkkea|g0K9G<2U(bz z(F?u35bdZ1Tdr`077H+@w6(Rp7EJoD@)rd%K^qzy zl{C=s?Q>wsQaW z^CCET2`4p9bO3l;fzy{`PW1om_Q19uvmU$-fa-{`rg6+c4g7&!iA9M|_xc5UY zeLbnHLZyahJXHc4 zH;gQCr)d#O`aqqBnLcc2Yc#lU=#eQ1R6Rr>^B9Sdl50pQal_5iyrXo0h>dNteYVU z;IFq1zF1P9V;_U;2{bzZKBJ)LYL&5~h&cN( zTgTpAemr)r_m^M5BnRy{4I-ObL9l{_0`?2=0ny-42IT`dX`zo$;5Q9G+2Goce48iI zZ3%po!AxZWRvFl!!XyUjk2G*C;TZIZLFa3~YkDXFg)4i4I$LtD7Q zn{tp@1BDCk05|)_1hcFU0?brn`p_vkoD0_k?Un(CdO-@7UH`y|;@`2<%N9Mu z%zPUAu;|1D&&ZwH(L1FSzo-as&OwikHg*_o>pl@s__HwKr-=bx+h1TJ&kJ}Opv_ER zb_3@L6Py6JFW}OkPdd2xp~X(H_|Ql>9OFd`ID2_&d4fU@y{cg@1*oE!CK}q@;SxK- zB)EN)v9_1Pzh-M1ez?M*dp9*T71sHgg`ZyT1k0x5Br3wz*DMo8@u1mmn6d=ib~ZR> z12K&j&TVaNmEKeV>;L4~zjHblDuug0lUdpRhB*AZgPV0nMct}Az~T%h*7TuAy8sdN z-(i76!acMf8=O>-Jp5;#0IIgojTGP4=De%-!E>5_RQZ-BsB+-K!E}+L(LHd90ENZ& z0JUY}pV~mP&;&5}gfShBjg9R>vXuXCfP+TDTiw8;w#_}iU*QWhr&nm=CWg#e!8-yy z%B3`8fAwu4@bBPdVhm%O&~iEe<3qibQt$X7gnV9`XO%DXE~cE zEgw6`N?-hKfs;)s=W4j=SPC!n@WpkWf(h4Kv?Kw1g}mX};0f5C*U@KzR!_JKpU-tot7|-U2phR@L_Yx zfu||x^Kcyzg%}IO%}y1I%~8PoYnk0Mr@e z{+2$;Dzm=%@C-u_Fd;H(nb@`ki5|EMK#YJk#)UR9+6D)vj#$%x?`;HHjz|;(PBISg zL;-iF0i++?LmAJX0|SKi3Ia94K9~`}W+n{64RFT*Q#)8I=tTxwE>QSD*78>5q1rt+ z&?Z`nL1~lV&pq)LRw{bbj0VSa1=1wSM*1@f8ONWzI6^cM+ZdJK#lqd46y2j2D=FZs z#S0!_XagUxH<^J+GCUOPAdmRZ;uf6HKcn4D&>pT}?uLHo4N=(ha>5dX8w)fHX!8?g z1gHbaQXIekxdcB5VK`f8;Uo|?@G5Y*aa}40XFL58H4(V|Ky7d%fMm20d@`{Zl<=@I zE&9P20_-Kg2L1uqA{`+JM;pyGkrgo&0a&Q{a%Mm!ZSI)xfG(Gn?%)&Jm*T z$YMEIInuCcNR-&<2y{a=CAV6_=5`(4a!Fsw6zR=!4JVCdVP(bD#6HI!;FlKcB0vh2|D-gQAb2XL{0FOl~z?HfLd^R_c;9R9~`;>uA+rf zM@gNlBR1Wo)swMRZTBCL*&Lev7>Pywu(ipn*th-p_s{wI#m|XtvBGl#(C(nU$~V9k zYpwX3w+tIo%FpTN-5Fq5SCp!IFWydcgzx?l9@>&HgY*d&p?H0}aY>-l)WASUE{hA> z9rCSkm#(Nktm<-(QoOEBXNDf2QsIXAPJyq%aO2VOlsfg35JK@uMXYBi(ezTy%$lvS ziq}q&@zf{f5sD;z#WUib+KKm?D&sxCUFiL&)yps(I>#4Gj-oooX)|VVz7JG~!@`!9 z6Q1B_^0U#f=p@JN_w8Km=#~(^sXD+QX7Y%O4A<-RdCN1W7hyP5IM&$o$EIZ3w^9UT z3X97?e9<^Ml!s5O= zvH~QocPykI{M~y0-5M`w&HJXs>= zdEF$QZ_Nei)NHOhDxs6IBO*{et2g*)Pe7RX zgodjydAw{io%4tasiRUkJ;J}T3NjLn7F`YGl7h2L$WM*HvM7I=JXu%#oA3^ceIFaC z7j~RgjXSq}Izw~vQ_5!xcO?INp_K7pxw`L^wsX9_yCF|fCNfE4b6p{ROJ$6!SGOgZ z@KsE^Cmnt;`orIaUa{tAU5(smS$7I^pjhO`jEm~;awHWk5_?G)pf>4dx+^M>7)PLD z6YiOYOIIqnnfTwq^IOT=kNE^hmb>vEph^n?PWw4n-$j~`&`cbHEhD*+< z<~pIQhEuuu%*}5E6-|0aPDI<}Xh;ZW$($OxWPShF@wL|MN^|ya+KkGNv-FNE(v|`Z zXM@&S#9_i@VXAcW*44BU>*5NFiufWn#t2sD67Cq|euRwT-M#~?vNt-|;ap_m+4c9# zuihvYX~$kaRo?EQiBR77n4O-4`*K>P=+(0KG3|0XQQM+p;;*ClH@){O-AnCzVyYOuz(4Ssba*x1 zu{`W?uN9rYg&0eW&2sx@VhYh8+gVIsnFxDAe+o^OkHw!dx>7R*#oV=2^Ls`e4DrGY z3+tN0R>^qo7O_6W`Z8QXBlbvi5MR(-BC7;>Gq9v99F5@T8@~Hs>62IifGv? zmjKrY6~>)m&Ofe%H^hpx*>4pP;`K@Y9{{32UB52lCQsh@!FtXD=W1yPxymTH7P3Vx z_1ud0S<8!ohzNPIY$1!C3mRyk22kimlspH5Mjc*3!dR%Tyy#gL5XH5oM(iKtH3niV>NOfs+WU*-37SM>28>l@L3f+j3D?+3zkDGj$g;BYLQ8z-4qQY9xM6J@_ zmLf_PHB5htlF~~T@`A-Zf!#=fYoNyP8(MP3D3vhM+QT;*W9BYvtBYh2p_VzxQM6sn zHM`hsu>J>_tl^%cJBHG2$?(5MRMSAM5JbsVpfF5?(ei|L!)SRCo19jP zvdX5vo+L97}WAcv2O!ZiwuC6X+lcGY$(wTwF2cWq@0F{Mx>(vp{M z`Lcm$A{0KRMus}S?a&x9_)HjgX$iR&i+i;r_N5`Lnxz7YV#GZeQb_EEmfS!Mp-Zee zSE3wd*@iRD7MyE7MFQx{%KhJeOaf8c}isiv>T- zxL789Rv`>c&4#zVJI<%R(8D`^zlm6UsmD~rk}T>)sCgN63E$YOlQdtr8oM_Mp|yiC zV3fui$>Z)F$9r-VMm>x%VWCESuk}I*j4?GO%D{rNxVTq%y`d#92(onz&8NMTR7^6O zS%yV6+QT2(hj-v^@Ur#YHM*cBuM8MEy5RkP+Qj9rXeAbtocrt)&wW9L8-Jl#4l9&P zNXP*JT2z~DF?AYSU$~YvA3g)C)hWDYOmLf}jX@cMrxdy29O+y8$$n=a`bc^SXQDRQ z7C~mk)WyAG#)XA4bN4Q6pa|L2P4gMArXwZEk5qG{E*^|AsKKX6U;nr0iDPx$caU)r zRD~{Y)1iQj|LhaZyy&tPEGsxil1Yo-`{NcKeqe-wV^wAu)BsCH$faiU)n;3imQlyn zHnH)~&cbPl1(ByH_OnwaTpczBAwYH|kuTeTeL*+rFFl3-i$hiSyIaUaNK>0^i{g9) zp37=Vlq|5Y(Z+*>h1}VN+|q_Za_BuH=-tEMFT+T41w#md-L;j}^Ztm5 zpMIQrY6vSON=`Mw`~Nh>i!N)%Dp^hu!m$0s1aE&&f=~Z#4u6SnAeU6Ks73dT(tKnL zv|qH2L~A^#QVT2MG!%(gLl7%V5D<4PbY}wn2Rl(slI#!m*H^X(g_2r%TNI+hAu!?f16=LSU~x1=+S1J$ksVyI$^5vSk@w z`zD9WUtUhe5u}7GUem$@_qhDxI?Yl>$xD7&tHt78DY3^_rvk?_5jdXNQeuf>%cf|N zON4(Y5IDAl9k+?UX$#hQ-AgnNwOw+XcA+wEA%6dK?6;hP>`vAFG)gR^n2 zJQ3vbJ9$?$EC-qDsQYfCD0z9paQd0>)^|0Bi+8&0cM(!el0W)TE8BOnq*)_NOvs|< z>2F2^(u8j`u2#5O<7pkr6@i9qFkwA@ako+mWZc4e>j}_Q-y4TodRs)>o7^hO2rzHn z3Fr1Mt~^o6_-u*&vTfKe*}A0Tc%~t0e!CR#QB}1rS}Y_}@Q!yU=;(|@O;hrq@NXet z{YHmB{df~yT}wCKB_-stEQzYNQ*?h*#^7p2UMX@~kyAc-t#Gx%(;7b{Xg}1Gl`+90 zR}elUdgI9F_b$nN)Ve@LmNs~LRyRC%&61OG1R({^D|aB)H81s08mIw+D0#UV4;jwC z0M0%yhCqXvOvt9}y+Xj5=O+2k$KoWDOB3-gxnvOyqE=OVzSg)(k=Kg6^2w=ylye~| zyISEHjc;_}+1DCHK+IYI7cQM_QL6^1(e@-Fh>l;nY1xo*VSGs&`_e5-J&Xow1RqtC z+x=urvAQEqwYSZQgyD5p+9YBIlm?@M#}ra7?ng>^@ynX{{r5T8&QkcKF9Xz4NA`^- zrvpOHX+=)^1#-@Zue+)kF?$g~>wuiK(Wq5%Oh>HFlTf=(BEja0GKg%C!?w2iex3?p zo!$$vMipVL@Rm1p@%mSH(A-r2Q(F^Ag$svab8m*fd)HCkaLHIzV==~X;!e%!r`ZT! zgRg?@9&HGui_&NrO3B%_4OhOoi8o#)kg~p{yyS!&EoN13HXj(@E3E@ER({zdUm{-= zh`B^#_6m}Q!H)?{Yn^rE@VVCYbOM3g(7J3F&_rt-(Xot5rB*mzcwr0w=Ywnb#x+}6 zzt*XKgY$&X{Cdbm5E#zdGRCLhd6eg$>f(s$v*jJzaPn54w&vM$!li@@FAy}vG^mh_ zBVslYAM-jA6{E`mC9i7=B*_g&f7A|-@bl; z&D|ct_Yqo83eXJeyL~=-wdU{s*dY;{Ies$)a>WH_8-!BeDe(P}m=)-VlojQqTtd#q zkW2|a^wB2HIy?9*EiH!j_QfUVWkJZ%Kx&~Q`&tK)uwN1(hyTv2;&VPEVK95G_}A7Kib6B6QJtb`5CY*2)S0Rok}(2oGu|`?UUDG)T}0H%1V** zd~&Xj>-l)T!dFY1Q&~&1BOw|l;=ip(#070F_1EVOrf59GY^StgO>>?LTedEaOWd-$SFA-BktoX|_&GpP9SRSS(AUBk zgReBo)A*i3c?#bPECr=kn{FiY{tQAO4K*#LUMM7jSFb*cbKAD?tDz_P`M|?v^I;)* z)4KCHrFk9K4LrmxL%WuAybY8ixEL?Z)i^2^v4n*%X4-MYEzKXCH^NEX2BAc-&fx|r zsB2q2dRjbQvKfB&f)oiOAscqX$x|roDuV$>#EHu!o0}xoaSJ^PBAJJ#EFzHyO9Tsh zmITWR#ucW#QrKp+;n0}Zzy;4qa`l^huK!t%E8o=0pMRo-kGy}3p~2ct@vc}mC-Hx_y#gr&m-d{(vHTeKjqNY|{xxgP=7U=ywyi&8APZ85DEmWZr1$SWy74^+fE{G zz&PMpfJIUZY)Qf4(ILhfdo!mcp3Mn~)9AL>(6Oh32mjVVSRS$FVd5P}Y2N-gU1!}+ zY~#}inTI&RMW8^&qDaV6Mjr$a*fzZWjmXzxhRQ}{Yg=> zWm&xZ%9n8U@4bPJuJ($yH$3@H})wzSxg>}(Kn z1(0DdXbA3>pQ$J{9x}!hjuiu}Y4UjUDS6^j1}D5QAPp!9LL&t{_gSOhj0I_tmIEdZ zev(z9nYJX1r7gDDCv#EDYdAG|J_(r$e7{AY|B^Ro66Dg&r2E$~@Yu81_tRI>zVl%= zzU+rIo%s+V;fIl+D0#@>Haa3|0~kSTo6TF_oZ>fkWO(-T61?iQ&D?gg%bj=D&O&$D z5Hi%JN1jp|&sWpl&KPY1=dtKnnNZKyLB6;IyTyhhIh?$~4+(jxV&%z;d$E|q`~UKf zc=0PPuFGQB4aXnj$~EWE6KmyQZirtGKUMbopB#ITm-U`btDR(b`Y88|?pvanTK;H` z3!bwr!3A65Z0dGsjthJRgK5PReIBjo3u zy)?b{N+BtCZF}1vy}52=46rYIoY&v~1-2x6cq)CAaaDfQJvwoO*WULzHY7TEeBuaM z?JeniEl0#{cT$C&k#1PgAqQS?9z!>zoX?-D+xbUM?#_hT`|K(Cd19cLtsF5mJ_HD3B@$ z4TT~?E~K|O5~tNh*ufWOAIQ~YELF|JBpc0>PT$F^-uUvmr-==F8HT<6GyZ38)E$1Ro(o{ zFGu&p261p1A%m$WQbF0%O23L7H*IsZh*Zl1QW#oLAaN`X4wI_37W^Ss(a3cF#yj zbWHJjAc|uWMzDHK4D28qBaskXc6p3DZgZ>pz9K9uLXNzRt1+!_dpOO|{v7$uNsJpU z;W5=qM1YJ-WYR*$iaxH!7@YmZRyYM6PYGdgIH9#p_(o-N1s;mXv(z$}Ub? z6JsQ!`1hN~`SC+(@{;}c z<0Ax|ai&A>syzLDqUwx`Wktvls#(~xT*JoCAEy7E>lo{`aWj4pX*OBJ*QA0s2@sY* zOjAy5Tup3$wh*B%WukU8hL)x#o^|f&b=z-~73XhuzM57$$+(~6&!7B0X_cSx{p%AQ zd}8}6kwUOPJIE)W`q2{3+R{S`_~4~&oU|rJ|ESN0zB|OOqq9o~{bPzx{AifIQN>%I z)6DxWY31R=d3GOLcnb^RNJPq677e=;8s8|^Ck-dHNQ9b!giB9J8Q!C60e*O|Y6wU< z3P%kj*WS)4uX!f}55I*255AVn=xT%t7RF@>AtS+Phl4l0kTu${?k!)Z?b&y;_VqUD z$Je7qyReL|Qnzc43o+#+=tG)%)vLobL*m1^=d#wNay-ajDxJiit z(r;LeyQuJR$SYcF$t8@g@ok7~3b`9O*7Us)IDC7cgYf6s>>9=)H-drrfV7Oz@$ z7JD)Se7EoJGJC~@ba>z9OE|N2BLErY@{?l^E$Lh>HCmIBb2i6ueZ%+fD{8lId3FnD zZ;GR};kt)2-11b8Gd9F|-8oG-R{3}PWWUGJQN{XBn;oki?%Mmzxa&w-urY2Sv`3TIL2pw)&MI_slOb`(9cYSU3xoSW0@ntqin4GNQy4AK z!s6-M&!^**2k3dp4G0k$8YO|8hP*5%Vfs9cKZQ$^T&-zZA-g$aayynhRFm0XOdg5M=wv1zzNr!J^pv8i2 z%K|@?4ogpT7ueejsWE6PM!W@5);c8c@HI<7%m&*EA2N8hBr4a*o?pC!-4CCQu)@4# z9X>1q=|T#O5NOZgiT}NnuJdolYAXrGl0KyE;yC4`hX@Ud6d(odZ8j|}g5$@j>U3R} zgiJ|vTMY#YAb&;+-U+G7-oI;{#9;YyVrlxBUefl5^@Yap4^RJ$u2>7tX+HreB0gt!B_B3?q;)+~E#xic0as1F42JbqB-ivN4jz#Do zw9Z;#X=RvMAw{4e3qKf_A4K=fO%|zCAy00>v$QM;xm31UX!;ufmSFVC)fJhu8>LC! zHo{C^)U~yZUl|zpbA0HrZ}W}~&*jQB=h5aQr@ZHAevJP-cq{+Ye-CPj&5)J@?C@Cg zN=V1Ci;`2$D?h+8-ZZ+rgpHlSsd;2{VGBjAxj3)6aVLM5HC0&=)C>YrQj!wUf?jxr zPd=?i1U%dVJszy~p~XNmkSHN$4DNV0hYvi5efyrz;LtYmZUp(F(Ri4fOtI!Yp~3P3*(J=SmO|3mM%6d7%aV}8 z@2_eK9DjZX`R$cO$7&!?%R@8gYevTL{%=i;93Z1z{&oM2{7?TqJf~wDCpE35*^V>p zrFn4d5WgAO!>FIFen-y`jD-tuT_r7)(s@JH)udvQ&0XcE+}7@?r{IO#67;rO45byj z`WIelw=TAto@5XGnf|4o7BGSiA}W;NsY4D_v8z> z_4apiY;-5uN+2D9h$3MT8o7yJozNmE6p4b{f?Y*G#i2EVq3@}Uoa$5ffF}KEN_3p@zvN49(9m6oKR?Fz`|ktpTk4@KYwSJlVT@s2hfRA*GLq4ZW;BoYd7Ql|&Wle;^3z8$JbJ`q zeTT*C&MC?jmK2<~CC-O0Ye!1Kx9?7qb{BrM*}^7aC6;%h_=(DNI=6Tx_T>D@KW#CJ{9_Q4xF*;Hf|8`Y3{iBNR zW{bEZ@Rh-~1RuP#jh2MOmhg*5GkoLD>Ppg=TQ?TZvmlcIXR?T5rvAH700QI$MjQ4& zb{0Ro@d~oSf<(SR;KCRLc4#aVlt|kqAYnU9=ui}+Wnm1$1g)G*`0#v#$w){kFcP9l zLKkplK)AMm<~-PLK+;M;UKB&`8rX0yi;f|)4X)rbG7pSh+{yTJ+AEHZt-FRvRb=QA zWnVnmmR)kTZ}Yc;o?BgKJFK)Y!+<3Say9+Q$eNA=8jIkQ0!T#pkRD zQs5NOnhqjopCCHUNytk~UT}Q4`Z-#*5psn2UURbBQVhIxZLyMO4yX~A?w=3MxgSxI zlf2@hA5WQrK8RqL5fdqO~Y#pQo|4!BRd#`Jg;dJ}94fas)&UvJ$ivlxr&)5Uwl1 zv7mP>jE-e++n$xdBPQ{DO*UKovAnFqz4_|MUT=*4KiPFsc2OC*Iex zD|Ede)G{VLWnh`)%GL^_S;OD|bcDPQm!6X3ldtS7Eb>j{4UhGC+<(AjD5G%|+_pQ< zg9r1xa7U62oz=xiTe+4K_yJ8EL1~3=mPwdxU|e_{8*{*Qf+$&q5wI^NMaGk&Q(?<=~Ky8y1(XYPC3bbDYJR8!nAgiKh zsF;HrW$jHk0WljGJgyiXubvT>H6fQ4$jo-_@jGpfd}IsxEy>C@OyKI)bnP+X$LF6` z6qIGJoP3KNt!qiDZ7n;YI;CX``Sj`iF4sz6> znDSOe%%Rhp7H}j<_u8YJwc|c+c=-9CJd9V&l)%_v2ZU@2glvHZ%%esDKte1-OX zEZ;{ckJVj=S+)A9pfXQOzz!DnGVx$wYX*Ws5LbpPg9Nh1gcGFV8Ns1GpYe>Tx?lXV zCgkudnl*xT*TgvTq0P9P5*52!8DrqVaXN1vng4i7qGY{7C^==vxTJL_*XEu;*!6D< zKNED`L*064d28HN;(>!M@BGRD7oU{mrKcr1c}{QP zymkWe`lMt-%3)7>={vbZ%{cD0M^K|1urf(#%2Ke@v;+Y`QzT>^YRe%pi$|Ne^rWBi z=;0mo4X;M{K1OSVRv6`g?PDwpUj~G1v@G~$2+v?VgYh)R)fmsm_=>p9asC-MV5vMz zgb8!T3J6yTSl<^Ef#hY;<;?;zPC(#}Kr6WCi7bX?Gs47eufT|lxci6dm-?XMNSQb_Fwlcgmng;73QOSBRw<>PB`TP-phS2M9S z!Pb8{RP1j^xoib_c!+9#V!i(xG{f z7AQX?WzQgdgAQ$!cdmY%=b!omtgHr{fLJUEEiNSdu%a-$K-dHkvn4>;Fq{A(F-Gv@ z{v7)URZW)qs}GedLF#>{V0FZTc0)oU!={3zO#ljm6;sb+5tb#6oZHIC87hv>LvXhzS9`9Q27hYKMD&snvDe?I3etRA=Vj*p|VZqRXi zkyFo%F&L#$nLOUW82&v+5MO)@jx|cIf4d-1!f;)zkGyYTj&*|Pl;j~#%1idF;dQr|xQ=*0hGbNeAg$qJLu8sYm0&jZ&*dwG z35Mn!<0q%+xNm&QuSiM;ujpa;6+P(G{B=;Hz!G3TU`NSy3SNPcY zs1(Uw5g(1Jtbd2ZRAEp)f87S-RkCM<}_f^(8@bfE-2I?*B-DEeXA(=U9{ zpOiza5|W{dIym~O9=vX6L2YS_2D#T`rB?sefauQs5!gF}- z)@S2uRdp|vzyJ7W_~}D;;=XAM_U2CHr0yp1Z|;Lfg0vw)5HSlOv4ubyfoIf;xu~rq z_}Y-y%k5$(7!y9(Uc3%ZCxZ}E414z5V9!RaY}>Gf|Q%(QtMWp z-*X?2x!c&C-$cLP&4_BoHx@~er`H-~gWb=m$vv!&9YTsc!UHf6bAvi-O+ds-6l<<+ z8Jc2*KYJ1eS|RO#6UeSVeQb>W30<=$7F82+n0SK5po|Gzl#D?e6B4m0A?6}08#6U7 zGJvrK2FS0DF?4YUgO_yTx7iEs4`wuoLyC=`+JhZh8CrxFjOu$5A`#eFcq?LgZ_?O> zAZAs6O^#_vY;PLzgD1*(5g&Xaf%Ru*OoK_A#5ZP&!{lBd!^0d^2L#y z&%x|6DGUh_EWa3j7C*|Xymg%AwW9hvu+|MC;$BF)CDC$er&1k7%b~$>XkIUrAt^JQ zZ9m9av4=1sYyua$x`{APIk*cq6q@V_6BK!vuq@gZ!Aht-`9VO=6HPGC47P>Ql0zdo ze)!}#q9)6E0cx_Sr$Yi(1qUUAHad8n=q|*RJx3^$ISgbrCwb~`P9VJ_RrDcUAdCia zz-QAx?je3GU)W8pOoGazP7|`0uhgTgpveBX~LnLl>HbSX$Kr|1ilYndw7r| zX%*Nh)acZ#K0-F2bwI{aYm5jYTon>@K*|PPM#@EV=wKTG;b08;wK4o;_16jQgWTh? z`O|xd9WO6DsHE7WQq&_ad9fGWs)g1XAq2J&h^$6@Z6CQI7w?7Zu{%<;UQu}4 z=p)1AWf2mym=rOK5@Hr+(laGqn-pK8gor62YM&B9rbx^-U>xY{20szVvs!Rx{|Ntk zFpQ3C7${ZEd|GQ<O>GCetg5qhDdZv=*n6Ib@B288Q}bf0I@+F-@g!1# zy=npxlL09UJ(ZZF7EV@4#2gXuY@{5eR!#YLI4IvtC1hj6(QfckUK90bCp5wi)&I27}%xY`&EDh+Kn46^3i zhi5JPsGaJw_#PLTbe7SPQM!A&YrN&^;kqt^1A}F+NdyiOaoch7LoRy4qmsOsf+S^YdCWg)!+#`ZmPX>|lLlT}$%pp0KlJT@eJo$MVVlIu6 zA&@U3Le>r(>jF0gj)jRyGOp&cM~<*7-+dd>`vKPe_n|qf@C0G6!tgt(&142zYOOIc zFv8;xKfWxbi$ezwv3J-0!q3qG8tKZ3$H%b`&acsCv<)dFjb#g*nUN4d-XG|%32kI+OY z@?LhWpt0#d3Z7U6$D1MRK+eX?NdDtUA3q(P2yJ+4zot`lgdEEl26qjTT-^*pV25Rp z+7xwU6{@R6Ql=O&>!4`BsL<8Pp5KQw+Mp&3i3iiH`{rTdN9V4+ zSz(%bEiG-0G2HZ{UvcW0r_kQsv1}Iive^tj`{6He^W|Cb(Od+i94vVH^HkVNqh(73 zThc^}ek+T3>Iq|3mc%0v7-9H9;s{A3AI$9_iG%h9`5}pwwQxFIWWq1$QW`xb>B}OF z@Je-KDAA88U*IdP4vSt>5(b-LupOim_#nq5o(JFb4)I^<uFL)R-p?<8bo2D{5RNa(LUE7? z^t?6pLMDrlq063Z$QUy~Xcx!imPeQAO2-^Z7g}h(kvIeh{v@}ZW@4ZPIwQ&EESx46 zr_D#ib&y$snM%xMgdFA;i{S3NNW`UtOp%n0z)U1ypcRo#7CbGCZ;|s2Ul#}Yyz0X* zkn!T2e>F*a>$*S9p*;hL9jl0Gjb%%u3?oHjP}&rv3uQD)X_TkYo(_CXRgiiilXKse z8g0;7gC6v0ziouxUku=k%|F+&G)h*~F5@C1ARCES!v!hN_4uEE{{hM7B+okgS#)-G zA!}t4G!GbK7#| z_R-lU*mHELM>9V}-Y@|vA7n_ld^CF^y(SiBWJvsR3oqm2q&)0qg-B?GRYJy;ka8(8 zQ>q(JB4qFrw`Mp^RZ6g+!UfmN5)Q`6ypvYA}yA zADz)?H;c#~uIQX}IUERBd3_9B9o#ROYKu|(J?0a*u?i3Lqp{TR|%i12~2&_=RbPVf)0r}&LM z8g>d=e8VPj_14QwLR6nlr7hU|_EpFittDm00&*25Xia4dz79y)RSM4!9$E!u)^>}H zjFm^nQTBz-YE)Wb_j&Z(JxcGLqr`_?Jc0$$stQNK|Eq9Yy2|ZM9>2!mEN9myaA!?HEfF#$1K25A+g38TPhf$#$f zLVGqsS%DLckw}VUY>Z?A#gAisT$9?zfaMlCSuZvjYoluChb$Fr{@O7P#%zY3+eH!^ zDMX>H+BXJYX?(5ll}0O#a3pqC27~*v6_kF>52(zOIE&U%kFA^D|%AwoO}m z2aX-%u7`#=bbMty=tc5{5QY)U<35#aP15gy1U`NMkTUbAN1v4>%8Tme#1kuHJ$fQ(0P9lp+zS|&8p*SBwIw=GK&}`2T@?)2)9_ItdwH! z(|h>ge_hM3e|$SySH7p{z>l&Z>Y3>~U3#2aXtW$`FqAQ(1Vo848h@dmO8Y##tB))H zzOn_(dP8U*k?zMjcsthE!KEHf?LS{hv{=X45HiMPcgu8k)u!K*k`4A${Ob>dz`~|9xVk@1hxlu6b;uoDlSFcnw&1i2Q15w zsP*=0m6u$W3b!~moxbPu$iolwoiBWkhwpe~!Q%{~ksw;Gx98eqZP_Z(mKPE&0|jpk z5y}_~(U;}eLE^vpR2^rcf$31F-i#K7g~3O0z)o|(&TzdsTBx22JM;w+u?Rf4BY$>{ z!GyWTg1~!4Vu2`er7&i3sy-_DoVCibSPB>nKB1*)mGc)Z7iz$zwiy>t%r5Z5G{9Pt z^6HJNh$Uhi*j0Um6!}*N)rL!x+k z1ykdy$sRTZqDSHoq*6*D%YIic)HKUtX;73cRMoOYQA@T7Nxqg^a!G!2Kr*!>gZ=}x zs{|;Glx3Y4`^x4D5TU+Yjf_$FOi(Dpmcqd@`nPW3@0o^D!VImoOHeHSC_Ra1;#iF) zkH$6@!GChv{VX9Lc#@2v zsu;`U!V)1Q+9>=QEc`XF^eE>|QT9|R{BR-IqMVplWZK(OAjo0}iaToMZ7iU$z_%DD zsSGb82DPkx2a3^hFyf_$rzey^!95YIOhXm*4PDFF1oud z`_k#9L}`><6F-VDN|sP?gR6CvT)N1|N+4wdFYrjhSPEO8>9R3&Snao#)i-`^VCBPL@bFPaVVh82j-(91^0ePv4k&ePt4)*~(6U3o#kD5P6-Ua2{&#_d zQH)TRU@RrM%T~CC5b4X47j`P$9#Gb;B&hgnEc`VPAh1C2ktVuBDS%cyeOZMWA`9JSvEnaS_zNQD1~E5KPRv#q zFBgEc`Vv6@kXA5JqDe87%$@QK0695c4yJkRwD|axEl=$DaRAdr7Ve<7wV5u*j`vLu+WOX2A;VH>I#Bm!ZNZTXDlQ$;~7QB zWl?esw;AOo8){!LQrOR=ZX-*T&9IR9iLPAHBHL1rz2+GilZe?V%gAWx%MBEvLL0GC zoq0ZT#xsnN%QG%&MV!--a;>EcC?{nRwo6?pEj=d}lLQyVrLBW2^~Z=V7PTN|j0rlH zgpP6zeYt_jDAk#*P-PCCm}iZepHYMi#X=EHm_uCCZ5O4BTIVMh_ECD$hZ)u@g_Mi) zBnqY1I;^Z)D%ExJ!e1#d`&FdM8DkJaU^n#T24+D)&M*pzm?US&h?)ykhR;Ak4p7Lr zsBx5BO3I6|$XEV3f|M?S2v7FqkI4&cR!L>ua*;8L#4Hw^!#tC|>>0lyV>B==icvGc z`i2ozmpaJx||TvRktYeW0u&O~UKV#2&%D=6oz#{|}7_~it+ zoMephaYZ@IwISx1aDr&rtdvV=1NBA_%mwFisieU(m5^aEs`sMpmXmU!IqG6X%9Bj) zmwLf$q2n)?F_tzPBN`V>N6ZT=E-BN{MW7fqvN0N%3#F2VSP6t2N7E`bDXEELkiwu5 z`znpnLuMRCh+TSO#D0fk!&YS=z1#@dvLxG1JAv(IY-jaoKkYaEjAo(BO{Gg37)B@0xRLbx z4hKolzkVBgcK4x_E{l?Df2$wRa&(ILgpqhO##-+!wC*G%v`AN8DH2BEGcbK+on9hh z?(OvWt#DlrDJ7O=1sy?*MjL|(uOLE%tGps?moB2$id}b$KZpMg-zqk8|>3go$|ZBN1o!w zul$JHZn&F#6vEcBHB+JaQxJuhk@6yXT9>x3QK1f{0Xt@M?q%n)ZpS)&UokN;!SL`X zv6w?5k-+zTlv3fd4vAVKr7V#4)YnR%%YT3L=m4Xm<80WlmaeW2q!jFXbU(lP-Yt0U z+?|>YrsR&N*gC47#2j-J&wthcFL`z!ZOuMHf@OiU1kwU&Nu({1wnRD-8Mmi9D_!f9ELRLG)84GUJm1X7|%z08kE6kf#+(z z|Faxl{vVHlp@t6qOou@)4I!U;!704*16Q%?#H~|)=Rf!m>G!`MD(sppT3Sf`{r`u3 z$|}tmY?rv140(Ng0a7tivpUb6;@*>vpVTe0+j^d-sva=E!6+ z*p`iLTPUs3N(Wu3g+R(6@|98sotg{JVM>mS-XoD=hh9iS9+Z5=JV_9JiR z^p`$w+A0;J6uGZ{wPG?R=8*i@$L1tsfK?}MFADoE$--!EYh~@WwWKG~eC?~>z;(R^8JsK>#u$>RI3NDV``B^jc7F4-yBMo* zo@ZM4i?T6H9f)}>6XTlyJ%OhWALTU{9U>Mph`^5%5qi5C6Rp7+v^L04`Z2~3G&7D` z8^nqaV}-|n2joF#F)9sy2IFPX`Fudgu0ng7>u!dB_)kM7SGTWjS-=*WD^AGJ94o#y zo;Ms$+w2{2IGV9Ts+)W)Y*(SnnQ<}F z)>3~d%J)6??>oq8r=Lt`Z%4(1Jo&Sfr=t~#d9iC2jS*ab_i9GQ9p3h`eI%1QXeBQ| zNP|!YqfBt7>)_ue{CBX*91ya>Mu-?f*r5=ug6P;}L1)0vpuH?Qp9=^%@1tGCe_t#4 z+_xm^w<|CPAZcrocKOND5*M5pSM2QY8O;kG8+6z+62muwm^5r|^Vrntu_~o;thrAz zPZ^G8EcT7s>>qP*wZImJZS5{6b$PVK&9p&G$~L5I&A7M7guK`xvXFwyuD+bh-}fe@ zGc!RLV{m_T?c~<6Gr~IaOyXC*Zb1=X>#A=4;9viYAOG_=xb|y5ExTQ=>a&Lbk1B2i zi=U@lwtoRiDWtTqt)*E6vMifeERJPaSoXpX-kBrjARTS7iTUm)x)1{1{))ZC1pz7h z24MxHEPRcy!z>6bOZw1DFjhds*aYMZ0v~WOItMC?_OqCL4xP_o@*XU@_@J*cm#+M@G>>tLAj^f>RTV)%L$4UJDdl0c$ zW$&MEY>vYffAVI!)^zh8<1xPUH(x^4@IG;JMW0~t42dEbV^B(==Fs4c zjG&jE-R@0O>#$zs7?T$+9@- zl#NWh=DB?9J2%g;Xt2$WaNDhSO#R$p)|Z(hHN zZR^u?wD~9*$QDK!v~Q3q$cjK4ga(WNBQZvXHj6>sp9yo1jh{z*d9>?d@-A-1<6pnt z$;h~c2wmx-)=|<3yx+3Y>pde5&+751Kn|`O4Dj@5eb!@aC|=WRb<=)MJj-+AY4ra6 zkk3ORG5a5Aje6>-$_9MSImpeMD|)ZVWKoYjhJWup_zypVK5_&zJUnd{$5{sU#ag(- zO;Ssyoa_=8k@Di`$_Ax;^bC={-?U}A7Ja+C2xCC?-VkQ;5ZI{{JwW{ z_g(km=H0U2MX7X)#UlaF7t6_mh*_5T9oKT#BNH)xc*|Pe^3nr{kcgFMkao1Frwbag z(ZNNcFg~HdD2(FH2P%7MYo;^@j+ zYkYrt$-+n*Z~GO<)^!!#U0k>=PE&mLgK+J-HJo_TP9A#T;VGY2YhAO&(i!J)saGC!K^T!#A0;P+=8C2<;-YvO z_;=k|5h0t=(ZY|StquFx7vQ|`g;+awB04)NA`z3#lK<{^aliXNn6dfi8OMd;%G4PD zI?+j{#`%V&X>o!;!jRNf!Z0Stve0_!x2Rh-AUd~IEcLXfD$~r?tXVaMkaehK*Xve$ zCStB-afz=a*WIz2cfWdXK*&C5JIq1WL6j^)W1v7+fr3G+@iEH7_%6ov&~A`-eEsc7 z`i{?hr&8$fjU(dQk?+3Xs&II+`TaxMArxTG&#{EmVk z!-Na9P%xUR(Kt&8x!Ne%@WR9dy;k}1pm*!9iM{T12y5nO%jEN545GcA*lT`=*vnsz z?ChNX5PStPtb1J`R#}P{C(`_HwwYRA6-(3NB*m6fwURKREDJqN+LTFk&nZ)sLaEA@ zd3}HC`?OX8Aun=-JQ=eUm*^T0^ZmOz=pQ`7x>fmrlqcmR2agh|#uyM9ln=_ocpko5?w;d3(Z|nT5{Wi-MscihmcBRRItKWUKmm$SqK9<(vLlKNXFXpc<`|V zM~@}2ON{a7`aA%3L04XBjlDuh2Nhp~ime9YB2@<4AHi1RIQ|G)IC!xhT)NpaVsqxI zscd99aV~lHUvT2{ULGtS1Mq6jdfCA4DN{c0Ldo zE>2G1O!tRclmFiLW<5DYTN}wg|7cZYT=qG7dr5xuqa;7@Cvz_-PJ@_$%M%m9E$AX4 zT8fGmR2_n=W8n)~(5?+4^q-hAC&N!eZn)wt9UD`Oj*iUuy%}S8df!2IKk>wr-)XJG z2zk+_uObv2>WGf*z*)`-m)#bEyB_bv^95QFM92j)Hlbt@O;Grx6Gn zo9=2WINdqOuzzA@pT`mw4o*og*X> zc4z#(cf&8y2M17XNK;^LW{lqf6NoJlPnSMNYWS$_F*@Nv3qOE3fsU76v z>ygIg@qGswnkX+ovYZ&Fz4pELPiGBEn*1>){pe*zm%zo6`I)}DAKtG1pT4#nf> z$RCsUiWUv0T>LvYiDdcrJdx+_U5AmzB^O^uF0}(~HDh~>xP`3$?U^ZlT5RUP+J zxL1T)vZ7LNU&96Q%IQzlPLb{a=JEC5y!u?41iPAoUmjhsDYt`(^>_K)}9we$v#g$9cL``Hx@z=b2MAl=h(3&M_ZeZZToY6-`WVq(-wP= zSUmc)#gQSgECmc>4ALLP(pf;D>=v{!2sCIHsqrs zI~#yxOB<=SE=F^Q02s~Wx$Vh5I$BZ;=GJ4WH1X{2@H2oBHd@5cRuW_^$m?jmC#q0RBUtRey4xiZFo1`gmdx5bzeDkp8aeR?|C=5fBu(w z{oQuQWwliFYrCfA_S2zs!7qxIBRzjU+Om{##`zT(>;pGr9lamjbrL4kGpqjfPU36V@9oS=8KaXm~XGo^GpE)36#LaBp?oLZN`^ppuHiQQa5r@$8jz^JIxPo$nn{4v@nrf#`?1MkZJ;{a|jbI+6kL@ zb0_Vs8%f$4qdW#O!^Bf?+J-I;4oDe$K-ODp&``nJ@^kjgnIN*^x?yt;^eqB zmm>1(s$~c38OIv_zcgBo^2BG$TP`)4pAEgMdN}vIvst}n6+?qV+;jK+96WHSX5%kP z7K=MiwZLTaA{j3Dsaa$Ur6q?461232dZCUQlM=SJ3YTg_$-*cF+D9wJ#6)0!{#PIB zXUB;NNVG$8JATVw;OS;O&m-$UkM`J?5mpDD^#^#q6TGgTOGo_6AlKqqzmMk&Jli8@ zpHExt)5M%%UiY$5wyqoHL!WJBXl$A3%f`pf_v3g&2vm4<2N#a^&Q)}->L`&klO$5D z9M6v9jvZuMZ%eche*N`-V%64D=vcdnmed>!(cBE(-B>$z5PRuMF!?jM zN_t{uBdC=SA(ziH1?eU*`;9AO%J*38sbemKr}&k2Kr_Vxl9*M^EpAf6~X&m zw&EvF$FUy+%i)|e2l<;n9OM0;Y9sG1lStWUto%_Bf_!2FMkWx_qBUicid(cdCnv2h z1&Ae^QSr?j&bzF&JW`g=`0+lz`^n$syjQ=Iji+CL?KIPOb@!1RV!PyYHk z(!<9%=Zd$}wc!LZ{xJ?eejj&z|BH+sJs8ff;AlRDYUv^-lT)@Kq@<&FGbDQ%%_uT? z70N9+q|RWu$AWY|vG6&ckTH8WNbTQJ61A5{B3W=yX&sG-P&xM`0FY+26BJ) zb8=t&A|jO{{+iz*cEuGnA?8XT;&H5P+i2OgjT3i|aqLI8*44SKY%G)VmZO4_LO1QG zRbUt)iy01xmtS!?fB4?t!^{T7NShX2#8sS#U(`?ABhK_&rnErGhr^WBxrdREb@h<>}#}Atm;*4 zUoRoq0jVxTqJy!FAl_U)j`1UVaNAl)tSf(g;?ORf?j0mbGDMAmj2EZ9sT)M#F!!8u z26)R$)BMK|TUZ_t`0=&HG(=(8(4C+;Vb8$_0#BwGHYrBanoY?ux}7ZPp<~?ipMQ(x z#F3VTm(La=6d^5Ix>jMw6ZrWY6GO+)%IDCd_i*UZdvOv;jMn&WQAf@wo88$C^2UN$ ztb(PKbhS0p(P}t8mgm^0hq01qI>O_oI?NegmsapimM%7(z8wH-^Je0&eiiwze`EHY zopTPNr>FAKc`i&$U?wKWef|r$Km93*cl<8)b1$rUn%r#I^sF85qgxA;J;k}LPNWRw z(Xyc!F-PA`pdnAw@gSAtci;8aDP&yOz7TBNzJsx`5i)aJSi8Hs>F8)j`94oRv3JVv zwOZQ3kZK{yqGeIY#we9BM$^GjP>_$K0we(@BA7zah%S^?w{$0viB?3S4UuR=dmaa# zypxVi7tr1wWWF3bcAS*f^z|KNP4g(JR1mElInqy;ZL@#>Znm^!h{Xck^3b7!tV`{q zE#)8*9iV+sdER`*aei{E;>h4KkS;`dieX!mT>%-FLnAqqYauC0dRzIW_wT{Bc3$*q zwq9^4(zemQPyen5xc_^fFTQOeTlRi{y}5Ssx^lTe3PDe6oSdr}%j)nWR!;=8j-XV! za6;*#^b}jOV`E{m<5yjUcjukx-Fs%-p7XrtRyAn7Z$Ft&d;Tvtcw=C zcIW1@#kOeTqSUl{Nf^N{$`}1gWejT;4f&MQPNuV~W7ch3mPJQL=bVIWSr$D#Jv{%S z3wV0ZK|HVg{wb0yB0Y8`SmFpNu;Vsfwpd168ZCz+1R`AY%ehvem!6sAdlLB~n-YB) zW3U~N$rK{if{3?bv_MUa&|wd8<1cUEgl*f1$7Ae%>PcSmTn9Ot;}<{w1>3f7!?rD+ z+VwcEeo>OF8Rw@z{W;sWp8y#4?tYMy&pv|6bYeHRA>yszWoYk6bH(#>T=RqGWmnag zaI6KLyJ=VR**o{4XSvwx8~+h){;}kHWY)A?DFiP(vy0d~*X4u|q~cbAkZXZC#g3T@ zMv|#GEj>lQ3DML<@Hq~xT&$macd-$5B;IC$(BnY>5N^=Evzo{#SrAD-vW`ut!=nbvHBbt*+=A)YEF`BF4uGI6Hg@hzV}XR?ju@SkiETg@2FDb^SD3y z5fktFJ@ViBHfEZNv05SctVNN(l$0gK3Ofn&uCA*FQ!Wr;VaRk5@^mIZ)4}(=irxoc z+cxcOQwvO@EDH#n%H~rZyc9^bBw4+AHEkU&Y(8}}U8_6GQU`zc&1pXUx5M;w7eA8> zjq!n^hK~seI=Xj>jt~h#VmTIAaj@fnM7uc<7U@ht#-pPn^z{W90^@y0Ne_*cyxteQ ze)tHPku(4!BO~|mzb*rJL&EQm22UtIhW)!0tuObpO#*oS7 zc=Esj9^JdQg00UVkM7;elLrov$>k~@(`d$HEK}QFOBIPGr3E8VDx952U?(S8F$sZl z>7|&VVRF}8Q)cWV*Q}|^g>A-(85tq>`Oo8CcO8j$zBAC5=g7k~+Th=QJN7weBP#H^ zuo7|XMEPf(PSY(&7{M>m~K#c^i#dR9u2%~sY%E5g{=RMVo# zSr$dhc2QBDZ@nQcNlQy`XV=ry&Bd2JkKGUL;l>~S3avET&+n$^M7Zk8N5~9KAO*Cy z=1C+aLW1uZG7}1s8%2z|U=M&53v-Y|hd1AagcG9NLUDBufG!FF>;^# zJVX&!b92pG`jk!R{{3V={&8Zjzmmke-#xoNB_RL3?~%La8j|mSANE;i&HmXf3o%1~ z?&+M{3JYy@)Ru$&%quu;rtg$rcML)ny4(%c*nax$5urKN?_&p(Z(o+KMLY-IiVYq4VeoO6L7DI~Tn zfK>RH4jL$(#~2s~RFYJNhSsQi?*pa4p8cZ<$42enPx>Pt zA%4}>#NT}NjHDZMcb_@xo^7G6uIX`t86#xZ_4v>K_!=Ml@E_9CyQ(Zw_I;n>p+QDR zhsjq+$iDA$-L=2OpQc;)j|rbkQ0dn$z&4cD;&q6y`uxmPh--@ zk=;A6QVA@30ePm{Lm!%&BP-iBlC;OUaO16{{T}j%A4H7q!s$8{M|fP&aRcMoR@@`^ zBS!XMb)7}CJI=FvuV-{D2BW)hGEXCPYiJx*;y6dpK1zt<_h4heG9%>(2Ej;LF_QKH zf#)yneN@)f93F~N8S30Iq;iC`?0P|}&kRH*guprfe60VthTIpvfEhTx_@5?~BKEr1 z&Ml1?gNfSMdp`NEdGiys3PUCn^Ky|f z^ifdRqFS$-$^C7&n7b88BUhj`maLJ_?|Ur0gv- zN-m6csRyuRaPXD7*zezfA=b_+seaR0mSK&)CdcRGzRWi6us zyz6fOrAhwzN2f&d%5_l{I>k+w9yg`BZn3`KXi;yLX#SqN9^k?IA7RJN6M|~JNUu|B z&gfVh!|>1ueMgQK7VV~e-yD^C%T316#uTI)%d$x(ljL)Gvg3J<9U34Z2e25xJ8mIu z3%#JF5JMt4^=`tS7)IwZz=pEd)z~09IaTlXM~-8~9fXvKrgec2v!BD{#}V0Kj5`2y z&pz)3QZ`|vymIjFcfXsz_=~^bV;}n%U;XM=mwGrg$ASv41p<}JF9c%E znnn2l)~TnG_=Dfann4?#7QRAwsPflu`^0jR26%&&m^RC*{`}RUUKke__|M_0vXK~3Th1Z4$N64ltU+gN3u_P0t9*cgnWm!~%XPt2x z&wc*0>Fn&FzyByV{qk0JKQ(g!(0t5=$}XZNl6fp7rfyA&JlEs+zz{-81`qGynjd|! z?Ds#rsgr{T1;4tphrAmcYs&ZWFVMu0NK0VHz)CrYrWQnVHzd1Ixh%>aK?xC5&xvVL zGs|o{D|{UTO5qJ31wba6v0K^^$qw*yn295Zj?Kk6o@LH$te_WTT`z$P$HzbZaZWt( zL@v7MB0l@s&mx52OJDlZQV*+USTG@buFuG!e%9~YTHg_;QpB&klKAVdT(GZ&fA4)W z|57P(|MIWcr=5mewF-cdLw$Je!lQ`NT;WoUxopv|_~wQ0XAUomXjdw=ihRcobgX68 z9X2N{{_oq*=Y`KbyX+*s>dmj`f4=iWKL4*@oYss;YZIm@&5url4jQ812_WjU=$9Tz z^YE>Y(9+w=p+^o(`TZRa^m4}oHO!%}1uP54@yH)Lh-q4fuq^E6)}SCHH;VC!0fZek zdzwyU5<=h*q{OWRQckDSy#DpC=iA@@HWyuV5uf?YX8@itq+FF(+<`~-aLUEcu8)6TF^l;G`Q!DYLlwA3S-(hrgjIVv=n^S(Tb#*=# zQKw~DF!x#fX!+1%2jQ_r+(Gfv6{mcrD7fNH8H788PZL&idzcO61eJlhxCkx73PXR= z!f7M2#{GOMSaLO(gTe(L}F*ZB9|i#qKzj?|Elp;%!Tx#^X5`!1Xg3m|5} z%$0fu$gaKsz3n9=QeAUiL{C|3bMa~2v%cp|SHF%Qe*edekBv{Ww<_l-G@~(vQsnw; z%QZ#7mcc)a0V#roIorXwljO7c9OA-4+A)wayaNmGOiI4r$me-BQkDW7Y>e@jS=r9? zn3$O0jcL*@nx4|pLZUjwRO%ax%nV= z>7~Rjy|iZAGsev6NLPSTYjT;z`2{^*mIzSr-1tAPOHHTT5z{25peGjW=`@VLVLp zR;*MzGLZ_3PW*g8_;wN)Db8bwQj_2tvu5u=2UiJZE0P6zujWA zTy$Kk*MeUy;pf42GYEGa;w`~KoMmG-cN98S$fk~IPfFR`IqmDhc94lCtat+QBN%rA z5pP3Ct9)sqcVbrtxiT31d(xm1d|yA zHv>Iez_`=Ax2VkrO($j|<)FmcrE;UGr9?iT=itGh>xX4ow9n8LXeFRZLWZNdi9TmE?y$SW}6k5{Oia);@s77!G@y!>T?VN($pEVOuMV zn4@O1a}x6`UKTPl5DK!DeMb}N3`(_O+cTHEyKbI+hmO{Fex_OQ^OivKG>C#69Jp%= zq-7y&2VpybxsXsqzQ{sKaN?*~JGNFZaSW6jWJL%WdR#1E972N1W85?*H;zfH#}NmA_rL%Br5@ITQ8gjwvDxJ% zSQm3i&SSyN`^is`t5*}h`l`BP?r2EXEEZM)zadH&?X(gTscw;{){cuAa8?PbtMc_dUYFJ-h2aH3LvPyyyj;`aA~Js0yWWM?n$LdrvwY$cpP02Tknt3>ZNQJMC7+R``$vN2n5iE8i9tpV9AW6G!%Q3=L?_on zvJ-E7fZ_c|7=G$76Me(zrcDrU#Tz`x@!bb-X$$VBDvXJui(2GYEvCK!Atbi2v1&sT zHAO5IxuR_w|wZqd!Ep)NX;ujE_Sy zg|KJnA=lbU?3J%1|CO&y`Q4aOJnAJGGnFp@E;c4-WK3E33&XXUvMd1P$X;T<`G5E) zy&m0h0$8ybC5#DI@YVYq{^)`%x$Hq4$3aMoY&OpmyY}*}Z~TA*duFcItJ}PU5=Nve zQzu<{JRJ)q3xhEtYOEJHu7!k*ln7}B56b~dp|WXEd8}k}U_oFz7~z1^f^d8c0^c*l zx;7#m3*)p9b+h(eL!xIh(h?AB1(6SW^$Ce>8%$mY>jXj+E)qc(shPWw6ZQ?XC*5U4 ze<8!d5p9-F=0!xtzxR8;_lzQAs1YFp?DjiYpFhmWNr^qrTw(8LKSMqKIEmkXPmn%W zg^|5c3jdya$bIhfG=26nGe*He2x2dP8ToI1v#gBTH->$#<}ts$R#Q_`OwP!N^ktvQ zye#C{L7Y4P5tHnOcTYE^^&ap_8!*yt*ZN z(Q^cj3^d`Zk|w8ypgdappghn%!m@*h?I0`%VJlSLMP~+)sZOk>7DOx_rmPt-69^$m zuG&Cy?T)}W$c_bHuCz$4-9mEB34lgeHh2Q0P0PlO*wVw*fC!^q6I>WP5AyEJUqI;v zZL;)J%u$v&#$FbSeDfh=DJ2~p9WyreErsWK3=a=4^~b9nA!l)T%x|aN9cDu;Ht*^^ zy?;NMKmQo^vo9d|fe#?NE6bOSVDfp?Baf2*&bRUJx;yArGRGK1R~Pm<=iuLVTd)md zIO4iI;J1^Z4*tzmV7iJwo0&D_^1lUmbttF2BIiq%2Y#%?b|N_uqJ z^K0>qlvt;nOzd}FgXoM5i>9U%7-PWq!F4ecY1ICG`1jq9|C>9|`}P+jH`|`p+qe`$ zN}LNX#J}w}j4=%QK2LZto?ZspaV3~s@n=WoLd(QY)}|Q2S5*=|pE;Q%VJv3d*+aX! z*m*)4tqe*DjPekE9()($=MYX4!igheag6UFj7BO=ZY+mm4f=D&m{Kpmt&uG=A^D2Q~%bx{(AEN{dG((iy0o7F?CIJc2v~E&p!JskdkrFV~=aN z*Xu?v6GE#Lk&L0kgR|1k(TfGk3RdTZHihP|h3dn4Nf@&fLDUNOJlf3_7at@R2dxZR zsgRJf2rmmx4(TK@u>?{D%C=>Ko454H1Wiq8gp&xgeCc53GcVc)KaX*AcJ11=B%i4iHP7kF;_#r?OQJo?)?}P!DT=P_u~=~6tXYfPx)u9b zr(^Bdf$Xfz!qfOZ?*INTdjI}8-z(RxpR$l#li_ z+U|r@D|WI8nMfgo!MGC`H%)FL$E|m*UC_MQp+8(i$_0r`NF3uVS$VbRdF-vQ^R(xA zJoZ>!9HVN6MM<(9G%0p%KAo1gT}IQUjkJUfPDLyR@%Vxl?bL%0p&oytqW4-So;Y{+ zX=51A<=EHS#@${w1E#)}gSAFFo#C;^cCqus?Y#Q6FJ)+OnB&KXIe6#@t*tHe_H?1N zMk|FaS!(yy@A0(I!A{oySVSBEJHil#}P9hl;B{VlD85=3K$SO9+rS1|&!D14D z9ju(yl$6KP4&V9dX5Mw>)A*jj_Y{ts4rB<+0U4~Nh*&c+(NxU3h{cg{4=bKPH8r6< z52Jj9@qfA z_f+&=(bI!{^2sy)UTaNXUqAha4sp+a{es8t-_P=Ethg{3Z5SFF=Fq_>$YyhlkEc1> zcbt~iCVG0ig7<`FU&a)ED&(w|{anHR29FIfF`g!u&C}GJqP4Aw)vKpUP*JZzS&~Ox zF9{kVvwFnCvhrGs0KC)5gKR zxvTbOpR?c_vw_K~anzSj8@&Q;o|Z2by)JmyEsu~M9OImKy_AWG2{vrlKq8TtSG25$ zhsk~6--|lj9B^LrB1lX<7){vB^5DUP3?1s{wlDsK{g2G8N3TTqzE3)xC6msQ%jU3c zi)?oGHdN!|V;mnWdR}yQ_0ZBX`^vND`FNgRh^lApqgVL9DOA<@)JMXIN}Q#M3P@^8 z%HO|XBdMg~;tP)>Ef6w;6=qEsV}gPaIu&|_XdX&tk zKwyLjBo8|l>dqSSqnO+XCOd@A4Dq9%#<=#T>RYQ#E*y!{;U?!N=a)f#@-qbiAW zaO~JIjy|-9Uw`Fij0}ubw#^j+F$ceBt-0ejx6;$o%h6-~tX{R2zC*htrT_qd07*na zRKCM(*szHW>o@VMTW(-@c&PCHj*d=Va@iFeJhY#KhxW5(^*W9o?W4DM6$1muc=iR) zn|(YpTt7=w^&%hW@`}SnoTC{Qt*V7mlK=kE7RJ*KuXu4kLTCi(;Eup(jP?*JkMWaX znwt}(xmod`iqCe6TJfw1O)ZQE?l{IvLw+2SA4O$G_`y%&eDT|>(7HC0I8!=YQBSsT z7AZejih35qFOU7(^-0=?v4Jsu^_d^A^PXpQ@w@8HeD z+thi{#mEio3qSk5&(P2i#|8$-436-~|K7&KH$8~*pLyDs&SegnG?wD9IvA7JB#JdR0YX+Ovd_VeHcX>P)D0`bO<7qsR=N0HfJbXX1O zVhY(D6O0_s@}J*q=NGqi)QpTL&)4L1xWelqkS=PSpKMtU?X5kGjvudUwvZ5lww7)z ztNs~R;@w*=H%U>l<2v-mYwm33)GMCN`sbX2)10ESql1o)4wA`aS$lZz$3G_f*?$TO zK`KF06V2EBJ96DRve_)d!@~>@596jY?7i&~?*Gvpj14WW+5A!?v7?)zx92ed`V0Ccsw!ZUChAIK_;AEWECK8DRt*xyz zH#K2he?9&e{vEuz^{HUB7t9-8kN3(~GB!R=E|zVLW)|4#vg`a0Jv6!R`5RWN# zZXM_B(?;38HN)D~F3l|hAw*#M6GqDj3xq_PJksQ+Y{wYzd`Uhp7#WouKJ0M+!%f_I ze;da~E$UIKVF(s}4H~bc*rqUt+Q)6A!YvpEjrfBtmzzXL$V)-W0Ey-V8_qm|_2=xM zV{EByxSquKLyM^}aty{^2U0fCg;SSZ1cK;U<7#h3 zbgo{GC=hb8gpiktlq19v4$bZu?IK63$deQv3E^X-3n^2<0U_|Q$Qg^QambzcEXI{h z|A7G}(z!-BpD(Jg`8CrX0etpL>mM;YZ&O ziHI&*=Piqt7db6%W{5CStI!;UqNEYS6c&jZH&F9OsUd!8bzNNwIVNRb0CiC&g$VML zR~jh?`N^h0%Jmp~=>^P$B1LH`oUL`Eif;iZ6BSD%Wx=F#uHeGG{2?f|_K8!6>X&CQ zigOu6tcA?XMLrZF8~%ut3wc0ObhX9Vw-vU|S}7>{V+W*Muck{P++Tq^L@;l*u$GMt z>`KCv@2!-KOHEk{SV5#L!-qp4V>Awrg&{f~B2q3~;1-=2ga1dJQObG=F4Dfsd(DYu z*iCNsu>;{TZ!P-qKDjSnDWs4%jZkRbkR_%DWe~+TOeN+OLdwDC2(Q>-w7j&nKPwke zq#S9x1#(>Eh_#fIqe!{Fq`Z>inR)5@wHtfl~5*OG_Wv~>dN(6 z>;tA|Uo7H&BVw_Ub&ED@HEyW)BA*DQ5>fKH#1<>@a#nxH$s}trw^lq~ZCOluEH#Rh zOd$)SAz>^W^(JLe_^+rb7qIB0ELPNA)zU2TMQMX2(ut7RjnHRK6oTSXlARJQ!!tNq zZjf><5D{cOGcZb1;_7j#3lS{xSt~U2qMmxP2jJ$6}N6bg6KNz#Pn8V$=EMj%kmo z$t3$s*Op6lNeKEoG3j8N_{96)0k+oAyMS3ylDF?_&5Z6uZP20t;TIidgRr^t}~E$0=v#g zIxYnjFTK1rsYT@1MzQ*#Ozj4*E|&jbo>t%*e(KsCazAA4qy0Z3hpr>XAoT z{ouo-28R#}kW7lcybV%bC`uzBpZc_CiuS1pNO>{$7o^{tDQ4}<9g(TM!Qj^6ViL7} zimdAc-{hd>lU^~Gvc-o%BR;tKQj-b|>Zg$Gd`TH*CFauiYujSzgcI2J{0kY_xdZLg zz8{!SKHa-^v+35`=-#ypYo3iw8>Cztlp5aGi(T3Q2jTAR=WQlJa6+OeW7! z&s%hYqp*=Qq&%*yBiD*@;uU4GuKr1}QHT!AwR=Ib|fdNPCM6_D#6Inrzp0 z(8*Tpmi8$PFf|6-)Y(JX7`*Xe$c|zB`F+aCdh%jkOr}Z~0W2(R6#fdjkS#XE2RC18 zn!>KrlOx$$N6X{AJv?&7D>!<}NlRmRZ=n0hU7URFk7+s5S8>49h?J|xjNPRdvn>+_ zO(2&P)XD|!Pi#4jiL-u(bjw!lfAIG{IR81SW9uqo0xNqz6e@~Q zvb$pBp2dVgV?C$+gipl6+`!4{Zo!)-V`{Wq`u?eJ7ii03&+{(i-m9--Z0%YW^(>qN zneHwQoqZMpXg_$cqG-7^WvH~HN&}UnID^w1TZ`I#qY;*133t)HqPx(QqrfjF%+vy>$ypLGxdl@Bvi0Qm zPmY#z&CNXUy4TQm>Zw6gT;aU8I+6`d4m(@pY;1DqjoY+15=RPrZ5a18138}qX^$tz z@*K!`jJlQG`lwi(Cti9vgIl-otnd5}$X`~xn&F`l*j9qgsU392x|iLBZZ>G67;yX9 zHSrjEUE8kq;)9!CY?^Q?%95BX6QlHt)WFE*B*)({Zv%mJ=v>!TL(ff<5_@4B$`t=@fg16kx%EC7#YLMxi#BQs;!yUu69~`+KI*Ec&^LX@e#(3kB}Z2pK}f) zeYaXUVJ=j&$S2osrSHWbK`-kf5W@-9C@-~kvt!^cGNWTWc4U}e?HytyU)gz3&(77x z7@F)>p4ano+G3rBJJ6K`V+_OIaemVOKaBW8wb^EnWf2l`1Tj0wR+=6+xM?G|z3r{! z+S=wE$jNPS-mgB~+5i1@dUo%raN>e3 z`XMPVhnp=Y5o5=>r*P(F&td!7C)3{BNuni%loDeM`Am+n<0Bk+bPo^Q@Eabv`99L4 ztLKee9+e(@Xb{rT;T9-Df=E)^+9 z>35ax%U!*b!OK3dcmt~v0>c{;yLgB508T!GpUb0idHkHqv5_=ic&MKzM)DQS4~DYz zw8g{=daht~a^q4@U)4B}d72;ff2$@%E(VSG$mSQDM7dwq0;1*e_aEQ1iC@3{?c`dT zXC1(%CWrT(*u~}RT1Z$^BM?_S^ zT;Dg&XCEJA&&2GJp_9pQ?$`dCp541EoV@4+H;bM%FdOWc!+9@%K9|1p)vVpN5g{wP zqZm0l$j#sQDL4K1b!5gS=KP!Hj#e&t`zv|g>o29Hvu#S2!A$!}kBsx{?_SR@zW4*i zhsvd);$kV~RgsHK%JaDZo<~~BBR5(9ecw4Mvy4h~yvej?EmQM8>TViTMY`;!iif*raf# zlI$#`3^PZ|Bdb^Qt9QL?*2vfvaP`)9-g{Cv?UN$nF;8*lK!zLoCb(xXOMliUt7bkl z5|&_9!shI*B$upd=Gi?d+9qvt+*AD9lS6!E_b~1(6@0OY2`>23m*_ZjsDjB0{%=A; zUW#nW)mztb)hGXeo#&rc5fM-M493v^^dY|Si7)WP9S_a;zLTEwEZ+3V-)Gh4wG|QZ zbo)7e@F?H-WFo*DdH+T5d4L@SVe7=9oMG#ndU=LL>fk(|=d1A!n zUyfGjx>$#TkSJMkov zBO^3D{KynC_H%ip=K+<;1NAHzX<#a>7q@TuL=1^R6iHUhuKoOLN1Ap~tP zi_6xwaL-_tBbn)A+DvEX|KHwu$H`G#Yx}A0p13*hYFDeAg+!19B4=!j4F;TTj3bWs z+V^t~*EwDL8s`kg={gf^f-wdRm@J_zlvi=JIq&4|`u^z2)01a5U|xR1j;6Y+tE;=; zI{nr;rx1pu@z9~HMq0d&a!Q7~GN5%)2k-mQr>Ls0E@{1rqOf?~N@|+wIQZNiUj4DR za_L*HM%7B2Dp64tmTg$g@jZv>?TYto5efHHIenC4*UUsFi@d45nG0TVVd)!D2&6ic*9dQTD+)#t#NpBo-K#;yB@F~aR{aBBQyV>_#A9hUn@VE?)2 zF+6u}PTd%Jz}4`|Bjr-YG`o#Aec`<&ktwX8C<^Dj<_bP@_jfq|buUTqo+u@3PCKvr z%uQ6sokx7&$W-Jtj!$eqRjXzfg)kZ04&H+{dWhNszb7y`g4gRcNjVVU=+b4JoI970 zFGj|Gub*dq1q`Zj3GHXU(mQ*gUR=BIv(E5hE0N1)Wi6)*i#8O6$6xgtMp{}e576jP zx$XS9%&Rn$2N_cGtz83rb$cKF0#jt>i;y9CtY;FBZg5UZ1&R>3HHGt9D!A|XIOD<8 z*D^y@nP_UFW9wFw9J0lwwYo}(bFROXi{5xuPLm-$hmP~?-H-9y{ZFue(=PO2i26BA zSxHt1fveJ;^MuP#cR%|#KhL2pdl>5O0}1Zh%B=gls@$lW#;!-7i|fPK-;{{R60?Fx z`kT&5PPUK@!yp(8pzHdSe(Wb6!*=LigftLRq75I!b@*X=Dp%tP=q4%qee_pX!K4@G zi5_&HpMQE)HtF`fJ5VGHk+P)s^NSX8I!H4ga^zA)tOb=wa&}W%5Q1=YO==RMW6M|4 zy=aN0b)gFQ%DOfdRoSDvd~4SL-`PDd<%@_Iz)$xKAtiirZ8Jg$=2qDF`FV3WJQ1MJ z7vk?HCU~NElE^qQ&^ezIr>voK>z164BjWVrQcm*OT@EgP+bgqrQ;ZD^^QW)f#-{uJ zl@J6X6v4t%m-D7CzL)l;^NMay&!OYo`OROm^RegfPkN)j+Z;AJmM`F%55AF==bo0n zpM+rJD=+4OTkmA3yFa1-;&UmNGueDB_jOznqAbS=1OkkVlqL`$H%Mv*H)HF4G;AgT z6a9!|Td9;f%L3ox={Yyz4G4TbA0fk_|LhIefE|x+;qO6L!5tTitW(fS=$oudg9&|d z3j46U&}Hd{tAuQCk??0brnMYE5&{ev&ej$7#E48>g*27z7hQ}Y6iY&W!{RzFofET` zet&3$@9r94iVWivNZ@CChnVBG@z$mF2q9Qe>tsc((`@g;I_@|!$|pDV;M2_~bMN^V zGH2H=v|#oKw-}k2q-^aPSQu8EeJU;U=VaXG7@1`%%cep$C+zrXW=bq;qZ}>QG z`JWH6=A!ilw`Kdo&+_w+e2arrVxT`Bz|FZMA z|EITG_G9FYt-QfEu}pLZ;d{GGq1^eyB;&f?$6{nW-VevNg47W}-NZi5IPy~-tiGO* zwBvTWsi>%6YgHrn_$w%WEuENjQL3EJ!Uf8TnXC`xc|IeUH)#owp!CO2sHlJw!alY# z&RQ1Ho40$(G6ve(Ew$y23LF2mx+xm^ylv3SXP)appDs2I!;s(!S<|GZ2(F)B!=O*+ z%UgQ^7;9>xd*wE(v;p)tPyt}j}WaXco5$NBO5zsBo6 za}(!We`#L6pi)Yn`16DO-{*crz?(VxOAm(l{TF}6+!YI1blP(3z7m4dF5AfcKf67p zlPtX<%3>wQ3=xgEN!e*9<#;j<->>8SaBK_6IA0zqu^-;VDdHr1PWvyM6%`zMY8&_7 z`4oC7$T-gHLP3msLiOk%c$9}c8Mgo*k2 zGtW+&M)QNKo2a#$)q6t*AAY7AZ)o~mXlm>VZ(UlSwp<9o>lW40>WYk2f`c2*!ccOb z;EJE*D}}?DYSUPJ`ihL(-1*I4CV4HSN5JRj*Ps4A_x0qR_E$>@SV0wQR>o``^R7X`~ zZQAm|?mnKn^PhRusfTp#y!luB{);~&6v*t28wv#Z{g-~moj3mqJ&#Oc0Q;Zam2seO zS65Ki+L)mSW29V8Z-|K7qRLcwcw~a4Ivp7s)?^%Mf5Ryka_G?;J`uN}jhMB3c;#wzO|xXZ8<*DO3X@*HH^fhN4@^H3hd=flLX3qFI1~}xM=A;@ zR-KYp$Hv|=1om7yIs@F*l{nnawB@@V+d{yXo7EZ*X-FRW*x zAG+C$t}Ea{4+TRU*!+C@eo_=_nlo9*iPmd*=|D`;anivgG145kMaGRzL=8y zXMAXs@7?qTHr@MJQS}dY_hl@%IWsS68KJTl zv5dvIXihAJrY~ghyMx2!jQhyzQ)N^5!usse!xOz8_Ko-x_MND2V6v`0>%OUdR=l1Z z3WhQcB84OtO2(e2w)4#!K286z9smZ8_wvmfKF!{zb`)K=&7OGy+mJt(De9>S8cCKe zrp8i^C*wG8iY#O-0sDpT#r4WBPYD@E$Vf$?7TYs%R-RBqG)*jks;CL&iXs>p8Daaj zEgU~_3{_1oR~Q)RXZyA-^z@#Lk$R+DRp{wC$@Xnq7#QeJDOXjMxu^uj&fGs(%x&~)=K66mb7 zan}_KxVXJ4<0v=i3-QsXy3u9g{<^9$*imTyRtqx81KtS_!3?C;zHEMh?MlS)Jzf0o z8$ZcYcRtKF-tY;I@98SKP9X&IRxe6hE~R9&e>i(Raeou^a)dx2@mY}4;K-4~;qt@idSXTyJ*0E= zs9ApG$l;V@1p)z%9z7B+?@HhY0r-4gjvqh9*w`q?jvh(jk5b+v1zpLL9i3s1eVOoa zDX^}|9o_El$Hz?FaXLRzva#9C-7j6p(y+g=l#(s|9)7%QfMXK@g7F#7fbl@cvaZ4Q zj)FQl_9`q2ddSTcIMPP~oqEXzYFiqLy%zu&JkiH3ANnSPC;E!73}{){&ayMp>+jxi z55ql!In_0F(nUzQaikr`rb!OGL|xDnL<7HooFxrylE{?A!1r` zaaMAIaVfF%)`~B$tu^NvDl03o*=&ghs!*s2r~9p{sy3yCxN=ohsj4z_ztq&!M5B)* zADhiaWu=*@tG2cdWTN3~w>xmTT;cNCgz{1k*m=K2OQ5g0&GI=|Q01WBX;vwvAU#}_uVVGoB4ODVadYm^T^aRRR{`P`-+=D0PUx^5YoBB*hwEUtF2sXuj+ zYow{AVE5GcV250?jK#x0>E-yIF6vsXN!L|X!3|$}AK!oXm(YV#nFKU5RGY>dKKpJo zd-_aceMe4Ot|*cY$^BTaHTfbT!-d+OcDsX`n%eXmNQrvqw`~c)R9lQa2+>(p@aZSeUw*BL&tdp!(mD9!Mo1$+GQMzn$v7URbb~Vt149~V{o)P7z-G6ly;ri8ODVD0ZE5SzR*nSC zX3N=czfk1;^!n`%yiS+piO;KwSwhDq0{BA4l;^AfsB@@%_x!nB++LZGcliVVI?g?Z zQ^S3IA<5D401K-f;q$Oto?pP}B;>GT%OQ-~3dKb6ZGGTruKD1bGN#=;>za#j*HrS; z4}JsRglB3yJr2$a7jOUOhgf^bhK$?!CcQj!_oGGhV|nu@i@5(sU`i3$ll9xvO=L#N z_^*}72i3lw&&2pQNW=GeGTL&!O z>mncZZ){+gCvN{oR%>$2MeF(CZJ$p_Z8?>ws;}V#w|zb<8A~bIbnn0DJ5tz63-Nk# zdA%+YVYAt&t*y)Yak<>7mE}Xmy-9w?7lpkk#LQa#x+HFJFCvP(Eo5C$2`Q?jmmI9e4`>I6@AZQ z>$=y1jAMp^G$f;aF*242IQy+|#V7Ile3msk91c`mq_+FvQr{Xzn{mN%hbc9KDa2 z5qNo!vA*HWgf9Gl_$Si4Jl59MPb(qETgfVUWP>Zw^^u0;$kV;-_-hvveO}!4PFB8j z9*fRzE8B`Ug7^LND1+YIm;clh zuqgt&s^HcX8k{Q4ZX3-mjg4&;w7N4}hvNaApY9w$&tt%@am2b}EMQpkt~5$+I4fZ* zIiI!o?)}jpQEfJ^eD|xeu6gIKuHdHce3ajQ?nnIVPye8#I+B2MuDz7kf9_q_vxGl` z>7alAPjBZh-}%4Nb|Nvaiy0+dgvq$RP%^&sBcRL-ubFJfn~+NYDJ47p(#7LH+7->S zGJJG`?rj4MA6?4&8Z+y5Jb&0+gax0#=I_6WX>y$NJH|M@BD^|p;4~;=v%NmGrQv1 zop#>%h4)g^RL^}s{v$>{BUQS#D+*V<<65ry;2U#V2sP*raL51tihun4u7cd$a_h&b zlP`B_(Y6;{g_(-%i5<(<{mE^D#O)LS0?GWhlSb<6(S!jE06rR4uXJ1u!u& zfyXmRLqlWMH`8K}_Z7DX0@!_CJkHoG^83~{r;u?xF6yY{Z`UlwttnBWRm-UJgbax% zsOM~Q+BHFERqP^ljruM3)%;$xVE$RvGDDygGBG9Lp$;TB(C=t z3AnL){O;O$W`Ox4A+{IHx-(~x)yiFeS{FKYRK-pnG7i zQkRwx{(NABd=@7;tFNLC-#Y+e`76g|K^8SzHv=fGL}*@aJ-ivzV|B} z+kL3yeU`2tr()Ts3@2SA8Gi&+Q#!NT9oTHyecrVkb~qfYT)CQwi3wU;Tg!Ss6*LsLoji^_)$H&we~2DWkftmXZFlS+ z=KdpN^m~E~cteZ_4aNgH<3WQ^K3*6SKK9rNkZ{f7+GtpGJl?#rfj{jVGAHMjhqL=CH_vP1rtf}~&XtR@ZY!na*zQC8@V#GQ;CN})8p{Wf^qGYWZUsY1szZ_Kp`8RtL z=c)y)eCd3e7S~39s;sJ_vZ|VNI%aiE4b?TZ*5v}~>g%bi&p-Wcz9`5qq@(*7if(GO zx&go4Gn8?nW;~$txu?4M)4pLIIX=OzK_5rQ0u1|fLdkh9bNGz;b>8>TQC@M^0lxC| zNq)Gsk6U*RMAs2p)!^bI>)K+Oq%wT!ohlcH6Y5JTxwmW7dRxPwsry*jE`(*|S)Lf` z?&n)KeuiC-ZOLikZ0?E$eBidvGk?vJjN8pyy_gT&_W7L2c+ZpDxcSEao+dI*Tw%m) z)D#?7QK)U4V@54yA!E z9y`JDvBD>jtYApkbkf77lco-;DkiyJy{HZ$;BBiLdFbdE4;>xPSkHA!>ZlF7yiEiR z?vD_15-P?4~HZ4xb-97npQH7 zN2DK1NdTIrQB_r)`l+k}DbZU`C$RJt2_g1YWNg3S-BeurWvXiGE$g$}i_0oF-7v$c zGstEo{-~;gv&xRDDJ8Fa>g?kp7G7AncPAq5VfO?LzO}iR3xB_d*ZlPmzVNJCliS8k zYnlo@OkU`j4D#_uj}r|0+0LtW@Wpf6({|chRy8ue#tdi(=mtODmc{~>l6m`fP02Yx z;fd_yp@(#C|DRv*r?1|clZ#u#d*br9y%No?aoJn0;f622A6KP2>%O62h}*yZ3;y`k zTW3r@=BY|Pl-Lg*f`LhV9cM9l_J1K&ds;GfUGqhRHoHkT9paPNTN2DvGv+K}na zCz)tj%kXJ$XLM*Hj*OKTJQ>F$b0)ayX3?3+$^{d$rt6%s{n@n4__e1_G930+sI;qm z>*Be%R8e+DmQwQhCr+5IYk(pIUp>E_OXpQ{-I6*knp;JqOXKE?=29D8-DD!5^W~>b znj~)VS-fitwoqv`@JtXTCX%QB@(4e8&zBh=9L>6|s;VqHeML^ucN4>7{P2BW;i)?x zp0d8l2a&!j_XM9|7<}>b|I6V+hX{p9Iq08cq<$$QYhI01wXkLUu74Xc&T1vU2ul>z zSF87Kr)G56k{b_=`1$%XJy9p`4Q-WtJ}e)U+e#ks>HOD2N29Vyqua*sU%rr^zGOas zx_S{$zF`IDcf>p^zO%WP-2-0Bx}1~aEPj6Lv|OBGR;o_M^N(-kTW|eLbXDnMF?gbn zZ@=}k?0WRMY3ZBX5gXjfMGS#Jfaf-E=d+*vE=LX@iCW4*|0I*0r{X#LP1rAb-@gqR z$0H|?i}1r1376jMpuF5Se2Kwzlwm{=jE*Do(MOR zl+Vw$_4DA7*a{tQIi->RJfkIVH@8AQPaL1%4|`&3wM9m3Ap{LBEw(cDi3z^5xi?Kc zl4aXB;qpu}^(g)8_R(Djx%q}qbF|1njpKW|_~s3t;_#0B)6g&ZVJfWT(j;xc*OD!W8ZGfGzb2FP)t)s88C8aE&OWyy$5$?HuDU0fyC_?awjdK9F`PtsG z&av1O!I^V1r-KaY1|NUqIKGf|OR7ePS+V`uGPW{31hJ564Vl-XsF~1ztcP#C<-d8y z%^zmP*=zIOf6r6f`RNC~&e*`nwDe0MD5o17m@6HB{)-=@bABh)Rh78h?woRbuy7rbBC63ukWJKwDcY6_u4`A!8v#S~B+gyafFI8R;q~gn19ATFG(tr4Y2Wwc~cX zX>M-8>2gK45yJe;Y45=0a?#c{2fM=&SBE)J&6(34E^m(uITvyNPi#*@d0d}H%FW-b z%Ms4Hk{uO`t^3P^18OIo#yyt%hL zEgAb}JQ>GZ$q>%y7Uv=Gu|;Hz9wvUjTr zpNH%I{%4#%Z`#PX9eo}y|J@!Q4o~z6pxUAGsdMJ=krlttPTBV$Lx?=u+@D>)eO6S9bUwIbW(vgs~Q zMkp8{Si+RNaqHa|Z-33{@8*>c-ofv$colk@oUT0+L9YMve%^CN3m@CqMupuR^*U=D zeBu0dK73Xy|L7Xyo&zI1bz;J_V6z>!(#DzXm0Y=~mdh8`Q0q{$PHyOiPUA$`(;b$&+_H#Kh1TZ~fo@qO!J%$zAGw>qT<~)AG|QF;bji1#>EZ7E!~FNT?Yv@X9jaoEKs64H z>z3DZ?Xo%ox}( ze%}0-L#%Ca^S*U0T)nIgXE;-n5CVs0TF#v{*%#1^hhI0i@4zVEdA5g5-IHjFU|m}U zoi%oPCW34_Imu)&&L7b*IP;kYS+o7A=tPgW=&}nwy)a-K8u-#x`5(;2@=BXn2^=440M!f}x&a22PAnQ&-E-@F0`p zV}we$R%N+iv)QPrt;gYT(0#O*lPCL1R=1`iafuK%)xd6q@Jc&@sI^?!4Jd&1yEozT zd-?N4uOwhgZ(T@WYtJNazxxoMeAvd-%j$U9l3F&lSK+ed{yO6e89Z}hf_n~(aL1k@ zPEN!opn|&O$>ZZZc|3JLRS$CBWB0Ob-}dO)Mo2lKk<-k_Q|Tq}LMP*V`m%paXLQ0# zQWQ$xb~flNNc^5 z0eayoF5(1@R1{T{aVc<_XbymW>N z&p4Mpcsp$;kEDIrqE_+@(2~vem{#)4ee7lra6xQdST={mjG6{qkv?{*w9V)j>jd69O=1Dg*)r z8z;90x$jSp@x*gambU(%FIq2ag()jU;v#eeKB?g`ino>lCY^5XIREABTCg%45Lrk^ zq@vKvCy>lLc92UR`3o-ZL~NO#WY?nbC*kp%W>S_&3?zQ0aCxE^C}emcfFN;JvPj+r zjxW1_cYN?#UU5bP<72}!>h)UFG^(oW`0H~+eEXY!q;~Iv)BQ4+6;et<7}#u;tZjV> z0vr7k#|aswThU#H|8L0B(!5=D;Iz3J3k|b#;9vNS(glu5j6_0?b(py#M?k7c)^Y^- zUvdd-U$mOLH@<{%cU1|yC_lJ-lbrL+L#*7lHQKI()j6_}@(i$&4J5%5hExALLxwf^ z=(LQ*<<%9HgA2|@S-yt(%}!?0f)pVGy88Th_iUl1>k$-V<_s3;Aj3o%L@%3BA(80P zz-t8X$>6N8zS9yi#PS8{Qs6Z-CZ&T=f`m~RG5Kr`{(0)TJh^HEKAU682+kG^uzJsP zY}ooQT;Xu&$R#qMXe&AXa+mvzBoaMrCC~iZY-S@DD>(w6y^_g_`h?X$ayy3U8KY;Y zyHF^Gj$?8V5g3_OGL~s2>jcYq2OHABXN2%de;KL2gng2UkfR%gt!2Md@fdcz*eI5i z0mfXFJhgH?Pp??VgtMYd#4K_8ysX~8m9@K`p=xq0rOok#Ta1-Fm4pnYl^iIu$Hl)3 za_PP$s>ajdDG-r6SoHV3+*U?|BR9}>kdP^E!A z7@&TnpSFQ++Ix@D-g^vpQkFJ@c=x6(R4@{fDBfAu z8T7N_*nU3qlBlG}~>IsntT5 z3$3a`MM!SE7blr0O~zaz@A7(jCU#U&=leuS*+*Ud1PeNx^wo9SznvLB}uDA=Piq72xDPNm|< zEdq#XBIO7nYercNj#|(#v^-K8< z5tc1x!TQ%lwJ=J`ib0hUpkkUy*%mpW3`CjD%8ghtSkxnu%V!A6wKUPcBg#F%B@l9% z+CQ5pS%m*n%GfJHgb6uM|MBTS(JhR~7dBx~p#-Q9{%IlQcq=&@;ii)yFA6<&^-+(eB7IqLo}qlzZ}RD(at23Ev}aE~k|&5Gb=iwortkgNTn~D+MW= zma*>qCq%rJoa%8=kd>URe1=T!&FoukHle(Wp_X;>Fg!^%yVl?^p_IAJ<~5z! z)1HbD5=GGwB_?GpOveiBL*M$^x(pgGwcUJ(_K4s;y+1b=yp3 z*{tMrq(2>aQMM=@BG*j-1~PyycJuH;Wjax zzZ~_?u25=-c!-FclOzd6P*Yn?V@o5AEe+H))=^bciPPmk(=-%CiF8IdI35fPLgB0* z3B)YoB%k;MV{yNyez)3A^6WEP_{ZNL$$P!Pgj^QoB4q_|5Ta7}7?aL$mbj@1mo%+p zLxQv*<6O=vfwZ%c`ZVXah&+JGiwTEHf=K=e6?0%O{bILC`7KJG&`6XuPeiE^Ns^R} zklmGTmaJUFinYsFv|=F*%?&tR4pdb&-wuR`)tATLLiS1bcl9YNdF+v=@cDds^+8H< zp>mS4QwgCP0mh{>+}*j=*SvNq8$R?VDq8BJzfJZI@ys`VMc=l4Y4?k0$uR{xebOPT zq(t#&oi0mUrt;~)i?T&&QO^D>r~6xEg=dAx8r!o6R&s)(C@fyNkTWk>&&tzQP+ePv zs;aSbjYDKX*nI!S?^|qLs;VHA{I9HKwzqPTvI@ABAiAM2Deckj&jiJ$arUR)M&qhQ z3BOl$v~c#P-o{^E|4EFX^=beSD_IREAV&(4-cWM3l5;LExfPW!UQEkbY9yr1EHPe` zEt1+R1d5bV*-ced)@)eK#V@;n&c$=F*=*76G7)OAq#QX$p>-CDwCOh!NLhrCvQ(rj zaf<-D6!;RBgR+Lbx`L|CR?D)g&Q|Qz75D~6t&fK}2xve^ntYsYyG%E2CRrlYN}gUu zj(6FbE<$!!SMtU$yodIs^SJj%f8^=A{+UqT(9z5rKldJLTN=3g`@duJeUDFDe`P~T zTFx?D-f|QE?4sm|)S|F()k3a%?Ms=zq%$f<6jo*el(ZW%f!pjtqp{bjo5je)4T74m+EE zc@Nv~ehed|=S8TwPVUX5TRt)nP1mZ_FS+SD*1zmL0N(V)_i=dJK6(##N6WAIz#Cb8 z!5L=x7v9UEEqfU58Jylu%YgFA7N(Az;Qm%(9eK7TuCJ)B;PTg8&bkZEz-G5ae=kIO zDAnJ5>nxNFvL>jHa+e~Jh7Pj03Awt{Rc zZ%)yOVr>&twl_1{)tyr2ZmQ$roBtnGbD99S;tTI)&1){>p>O<}$`pdET7pce~S0Lq|&jAvbh1N58vkDygiip3#I174eRFQPi^3BOmiHFDk;-opd${i$qO-{s zGhj85>69(vPu-mLwrX9he5NBJTd0(CwU2TVetxcG(*Yp_XI{F2D_(mA4wnNc%*qYv zg1%9W?m;^z2krEa+87wMF+8C%HmNe^QSo>cynd5d4FjYhOiwHWq!iIbo#TH)#zGAu z2CxJ^`Q{-O%o$H5{BlSZNmtdh5`}Iy5QTj2oTiJ;Ng?CN#ppQqRMx)pTDJV)k01m! z^IB;+V+D&}cCJNDk+T&ndD*$hP>8;*`xxmu0fyw%o35kd+*2(dcFkd9$;Imu2su@R zNx#HXmM)qxw|u!B?El?RPM_oy*`nmN%Nb3`imGtgt1si6m!2K$ssKJ}f~QrXr>+q#uhv^ApGv@{k}?6=l+D``1n1x83`qPw5o&Cj#ws`JuXUQ$Yi zhSR?u=RCQWu~jnzkw~zazqI4*K*C$ctdT8Di*BZ_x2LEISHAJ3oO#KaQ8%${lWf5d zY&uZIeVglfdT$kDlbT7sh}08|KWQsJ)m+Qd>|@iU`-?dq-)16R;*uF#APfF`*-YOa@Vnp zgqrdEb10uFH~ou2BBe!nWs7oK;HE2?F@uKZvtM#H8!taAhsij2a&HyiySIbGy{@RH z8|}CfTfO=0W4B3*_7?8Zq7`z@6Fo1CoG1B2NayJ7_pvgus7R$~(l}q3KDmqpzLbK;Rw&n~&H|RZlf(L)`SDv{2L8Os*_E&%44ZP%?*G2!W zXo>ks-v5*TPH>i2lgdAF`!^GkGyV5V-b??{lT+A}sgMZt`6~&o-iSVDF|wjM>J=2r zVGz5GMBMMO?;>^kq<@#L!3M^}7{L<<@b7q>;Gtba?$QWJlrt|!zF4|;2^U{`5kkrA zDKY6+_|83@+`GBabSQ|R%u+8m1|rfoh$&Y&{^*2jVO$~Ci#pfw=y<5N^Mp>KpaA+pT7N=f$E^qkX52D%1N5t`%yJ8`4{>p#Q*wMmW-}+5L znW8FaasHyoP_wi#`5dO(@xrL8!*j)rgyye| z0x2S=eE^#+QL>19FT_SRm$E7nX3EZ3a(h=t8+Gf}qF8uSD0Vxieewg;)YfLK(-g%e zUXO>Np#geMp5WNgE)I7cr0dXr4tE{o*wHR}PM%n1!>N=WB8%kXU*jx@?{*gCN85u{OIBzAA zmy`t^V%qcJtm_R&byvj*P7a!F0c6D=)=9fTkKz?#YFNvZ1IaIx@*8gao0}biw(o z`TD0(?RNZw!<@L|K92qQ9(>)s2u)+{|9pz33pZx;fY9|2W1}NXOpK%JdYbJFLLq}t zD1_JRVU%GMMWwQ;ikj+LR833k$+6(lGkNL%evmsq_)P*Ko$5J_ocgLuS$plpR5a8O z@&~!|!{1`hLz`x-Y%zD$B5LD&tfvw-hn>~upTWbwzGqsFUrtEP#>7kCMyMt{C|g-g ziF?FtKz2(j;kz`gWEtC6L`b<5l7=eG{X<`5Gb=j3yhJPWQK!8&arr3#v}!j?|KClhcGEI;G}N*1?XPG4jjv|lv8OSDp^Ri~7zU#w!;FrO zAZ7j&4~C&LHaf!i*cjC{HPqJDTk6z82o_wlj+gzUV=e0`+^l|C%+EF=RHl@lCjYdydDpYP0iTtj+E_$5G=c5 zW7;;=ceb+rjaTx>5C4>Qo9XnnSbXN{GS!?L7OY*0-DxM_3ruS}N(qU;yZT&=@Ef0$ zVUmoqTtyD#8vCAu6-NBO!#{+*3PjR&g%Yz60_Uk0;NSXK_$;SyV(+x`PNTlrOrdK< z#@>L+7j9on-2 zU6FT)?z-2%gy;VF55@;ZtS>T|Z|ri(7S1X+?JE|RsrKAZ-_}Iq+*Z2xADz~AloCc| z4WYW$D6ayk8bTZ~%Q&N0Sp06aFX4Ztl5za^I0m-Zrx0RR8`@B+YS71qnXaVut_m0H zE;18JrOPdG+oN;YeWEJfw>G5@6h-pNa}ECY-Ga6TCGY1NK+^r1eVW%y|^42d4{;PH4E7#Lvxfde!*H?wfT0xBvJXJiKl`>6-qd6KfT z+Ks!u+LDk{uUQsZ!|ME#>dO^#md(RmTa~ul&~^MCf0iR=hs|M6>p3BWVA+|cl!cIs zofIh)hNA+*=1lVt=0L7V-(!RwS^Zq(K*sUkW!!h`*XmsF8)0`MoRx_2k+A#Q)F-W9 ze#UZY8fwyN$2*T#ap&e%Qe9JG=WMe{KK(|W54~DNQ;RowTn@pFMcVpnre`3l}i2vok?I z9v&J*Rn@c+g2|9la_s0a#z#gXzgV6}^p`YURt_C`;rdk>_2V56Jk4$I`&!EHI+6gn zXMelueXr%}54<65dAyG7pUiKWnoI}#OzH^%ilU?IfMy}i9iDVdz`F*!6w<*`0$PmEv;2T{Bsgl-^J z1;b?{SnI~yUc=bjda4?$sHkvp;J^XK$H!T{Y9*=~o2(li8l<_UEvL3Uzk4@N|M71G z2FA1MZBwnsE!vP-KR)pE&a~~!C+v6MGdnG#ry^Lmb{RH@ouEHJ*N**2Ll(4TO)`d0 z4lq76TE-qs4?`x)4rH-lbGMk2NnM6DNGXJ+nAEq}^6Z$>`_^hBOe0h(_w{*tvt=-d=QFXRvn|`{w6a zb^jAA-n5Ix6TLVmJSe(u5{qu2P55ygAEy54!!+KrgTT{=80;Me1pWQ}?A*26?40m1 z3~4YnI+F2~*yHi==)({5=nwxu|6R|4kvRibE)#p1%JLP7TvZjcuUwF^Zx24bBjfRM zMfd)rObm^sEw5{9jQS(*eR@Ypv6K)D!K4o+ybuV2kwd2lA-L!ag-^f5Fc;*AbyiAyBHtr~Z0%27 z7$YlYV-=Xfb<@qeA zIWauOiG4@XmPd4C03)RHo6r1!fUh_r?K^UkfBgKeqWh^(#Kx7Ph50MyC-iar-$VUwc6V1NN_ue* zPfdlv&F=Ve%19O^+;RJu=`U}SVO z;dm1h z({6t8@$VE&%3}k=-17haciQ9%u(CpPBj!X)TAC4g7TZtwbOMtjeBS<5?smME&)C1t z#Mmg_fMH3lxqOchb0qrDNG9eqkMLAQ?71u5G`2NbK66q^9@*1GP>+)cA`s?DyzdnT z^IOy3wNgU&0KD^m;Jo+2&3}NWcEOPzIMxT-4#2POhgW_QUh@Co+2=EpvcoR;?AxR{ zS(k;BRR!#tVQnE7Xn>`hsVQAVGdmZA#wClQ0V46xb)8@!fDnr5;wE9w-n|%x!Q{|5 zosVt7Jw9n!r(J|_icr>e*~WahAKJr6{|Jhz($&?4IdNznGZ+Y1MgZFEcG{NAOZh#b zy{1FqzA5=*Q5OH>1JCTtSf1@%lCl4pT`7r#;<50wWeJ`b&;0dKzVo)v(c5(*=VM4I zIllKW-+tTY=-Rfgto@g@Eu@nNawEs&eIvviAW>&0 z3&ia&DMSiM+&4BzM9NYzHB)C|j`o~mJM&C;}z!6`!6$gXqShb9UBYcGZ&@pwFp z##zY-VYVaX7=n386J{3SDVF7#WQkJU^sed(+E--LkFyhUKJ7WN?+6pa8Fb{|0T2a#Oa<=2s$!%MJPS&{wY4o-}?BaMo0 zAOKfw&}pel-^RWXc*Eym_u<^zGcp11`Z_$mBRwgrir~g8iOs5}A!Ucnh8>7=4QgMQpMME~LL zto7urBk#|qBd1#J-f<82{NN9K?q%=idpCU{VS!VC?GHc8&p-MtKKHWsbJw?igJ*1# zX-5`0!paJ@B1GMg1cCdi+=>>-e-1ve@8jV(RP5J;|bOXNhOW1KJ|2B)KY&F$r zRaFSpT0hqHP}FiZq@=%p0D!99U8s8Y{1po31fO5(1hd$CiY^H}*M+)tE@NY3;d6?( z*u_?fjYq?RcHVjSxA2Wk;2oJ@qJNm7!zX#}j}LHi&yj*(LrURdv}paRj5(waZ`n&I z(>ojSmQn8h$orn&!Sb`#q%B{%;S?VJ^}Uv5-U$!eA9|LH-gp%)^J6Pf-}j?Ga$xiG zMfOPPkw9E&13e1p(t8^KtiV+SoNLs?Fz;;;@azYk2 z49wNpA}tEI)-6PlHjPj)lw*xE7cvbQXZju?W+?@Mpn=!x zqfPXaoL=S<^)XuL1tCl#*1{iEj2?heSjCx!&R4H-Cj=U0!G=oK| zFLS!7@N)ViD?-6p;Yu42(lh3?4zLl>R4dcU6;i@qA2;3FioqWrhSyzc%Au(}1FIJb zp4%7qLlP-hx%F`9v_*3L0y{OMg$#tls#C3Gdt*J$IjQT{MP8GVM>fNV{AT{{3A@FK z2`>&$ZjoL>m_vhI=x93#!eL=gpRp(UW|;=Wm{8}bO}EaNjemE z&vJ}TLXPOJiJ{7nPc)to3dbwg;U2#$ z^*RcHj1aL+Gl@A&%BU(fRiG)l8NL^}KO|^c6@Fy^r)WPr+u{g#H5n`&Zjg=M#p%?>sHDvE)w z=a!~3ewEmy+7fzCvyt@b50GDCA;I0Q zH?bnHi3|OS0WLBkQP?z-n3JqzRK@%VC2R%3NxhvP_&-TVYB{9-gxEl0LCk4TPUkl% zWz=mlm9S)N49Rium{wS{b527bfiL3E(J*w30w(cuS}`ONRvn)a&M*uN!$`3xL!l6w zrdiaHNXQO{spskyxh4Eb?i+{D@rw{4nR%{QtEoT;1)JTTaC|G$H4KTa>)7o|((i_; zqhfu(ltPwBMomjS@4V%|nX`OB@s*Vs6*ZNd@se{`cIGL3`>mhl@Q(e{-K2sblP+Rn zYR%Lf$w1138pO`GtG`CwNIz?XPnh*YHRCukwrNOBOR$nr6{I3SD0J%`e0A*e^cro6 zIWFQ3nKH2lNfy3Ez(o?uIjtw9BoxxqYRFDax9meCc>FT0>8C0%r=j@v)l`_i&gn4W zCGCJA6v^DCK=eG5NjVq{nF~PYiL8!@@xIBimPsbZ#yNiUNQ$4S9?~%^f(aBwG4o1< ziMhJkoP7<|RU`9UfjSbsXGcfLM1iV;qqQDI2;6RWEMiX)mePb_=!8Ovo(g8>x1&sq zji(Kxj>Ai@$nXb;oi4GI&kuiv6QQ}O0V-IxFY2C`&P6ZHIh&Kb z<)LLMC4Mu}u%+u;jVqAWt?Y20LOMPZ0ydUljfPVeLZ#EXOiIbYZi@je{M}_YXsHia zxVxE{n3%NW!EDHRO}%6*S&WRb`Io<8&zb9}s;;K8vXY7l(;97SYr~eZlCVHg)aa<; za5$;1u4c>_CD2-rBTrFCVEQH7bRB;hk4o2q7F3rVo5O+4W{+L_G&;Ag>)~v329uMM zOiWBLHa5nICwCGUp2(?9;j3fmOA{MlGs`~i+A3CFxGv+dLct)DLt`kKN=03DMwhr! zz&Gi|J3fiS?ZQ>%POGVRET7NZ6$_?4&?9fe2T^R zB3+mtTTD+5TggA^dmp{QRtuRTVx9@oz2;jd%(sXRiI$79t+Qts29t5lzR4DHOSRW* zyhWC`{xLz{h{2qO)QLSI;HtCXbGMn3a)shm7bRpRO@IV;c3HfZg!{a?Hb{MC$nw0T zl#GsyrCk#=6LOX;lHK5hVNi9?KL|d%m4UiydaJ9@tE&iARdW8D-bhj+z=nZcga}9ttGsO0XPFjCL%nGwM^8GkNO;kWeLqf!qCDKeX7Dd*D;>=uE?ORS`c%#_FBT+DBJux?W~)y&|%EK{vbTPS8~nS*3Dlq2i|x&{P?b%Ta$oO7sD&gwe~><=oyskIjSV~ zWn3pMY4?WnSYRo~^z{v-jZ)2oyjGn~3>E>Q>)22BVn5j%e+?K~vo?j0!6d@?QDfnP zh3wtC7w@u8sy085Hk#cEO=u6Mmh7_#6tOkcP_=FeQb2P{O9F9NY1{za&+TF2H&5Ud z!n{qB*Wx4dja%%E9esYnDMORd_1qqQ`JZ2Bsv2tx zY(ZRG41))5y^A|<{xv~=d}O1S-T&Ico+r0+>5Z@8+K;?BA;+{30!35vKi~{N5)Rrstj>%@LrRU71e^6)l^~*ThHe=Kgr*REIsqt-Ril8ocgwfT8|^P!tD3V2mY#^(E<;P!9)B8iM+Fm#>2-+BlC{Pn#BSpqW= znGy?GXLd`kF*obSS*~vj6S4^ZRiea9GBJnsWNEjtwEid^?(W26$7|L}ZOSFIIvdJG z$^9q#6FeW2w<1;GRtieIw}`-&_a3`3x-2&D#@zno9W5|AX_^q_>U@ zL-O;#g&SefHoGP{WAQ}7ImQ1q41+@lj}+8jc?*w;+2E`@Yi#RwJd=|a`wBFhEpBp6 zVa=MgI9)D$3)`7EV|o535GG|ct}XSKpN{*KPE=K;vvXcT`pwAklQB6lN&n_OWQ+gH z#k~zkc?xnN>`eIQt#@T4;|TWtYb$#meXhWxm;||r!aBavb%x;S;Blh9CIs7`6EKihERFLcM%`z{Ra1pT}epTOk!5z{FLo>8Y@R* zUJ_QvB$b?FmbBT{_(38aVhQWZL&$!gpWdS;7g&+d_{;c#zy!$1gV zYk*rm3XgmjKJY47zZ~W?LQ5U2T3~9;w|^1-{!NpVGm)`BAoVIuRN%Oj#+!hT3hucQ=)fZB=aXE&7MGF>D-;n66sHxVI zdAhC>)I*%}iMP|bb{P+T=~jXh9%9pYiPxo^I3h0fb2T4UC67{_EMKOXx4&jdGOpt7@PHUFACk9W=gC7sUx zn53bp9P_j#Mh=3dH-dE=Bi~I%mN}5ELh0|@NM|0})y1My7FkYy&95J4dCMp}Pu7K9 zL=w+s)Fb%7Pb7c*xJ4_63XliOgQ56=y{Cuwe<>zTy(wS=fXX~4SEGZksA;dprfEM&uMbf z!J4#zyTZ&hMh)%2}cJhJ+3#qTK z=b2}op{M60;}@;Nd;aM-PV{5z89*7GL#M14Ys8bI z7fMHlg-~AdbdbBVWf zu6*0K?xMeMu;6wS6ipD*k`2JX(b9~gwIyjAL1$+t2M->w9N8iysl-L^2_dMht>d!G zF5{9*E~T}#4a083x3C@0d8gyO`a-;~zMQ}-FD7vD211KF(KU_Q+B(**J&n^(Uq@wS zRYHA=lAesElpHv4AUg3EkLtEYs@n>S&&;F~CzR)UkyMop*Ikm^u@!~&*IiQ7P1~}t zoyF@`=Cp0=q7D|Fmj7hzY((Up(Aw)EbwbUM3Vs}fq=u9tOY|QE4Go`e4_naIR&UhX zBYe!nWuh`$%;{h*He5z4S;CGdccyq-ME+`u!42#8VN2GM`-uQ`(t9ht4jn{wXrlKX1*@&psoM6dene!rI z<>nn^LYnR3kz_H){HRIDjhxF}R?b+@69-SweYl(Uh3%HQ>x$N4u3UMHyLQe?)RN=Q zJE#jj^&6Ef2X(&rc0p@>asKXJzvSDu>wNilD!z0+$KhkI=$Pa+=k!JkQj#SB`}ZAU z=Z-x^wW;81kg)#V-~hWGdbAAIu zH`w%#O&GfMD$N3T{TcgN+7$Q9Sv+h~3jXwj%K8rqzW3J<;~pcg>n5cn7?RxgjLwB0 zGWhRbs`vsTJpTA&oPYj>Y4Z)28ky=e44uaxe~dsNz`(!&t*xyI+jk#7&Y}N% z0K%({WJaP1U5c6NU?51)7r>De23^%y%T2d_hTZ?%#J2k$r|-}SRJ)CiRSVg0?IpCY zTu`#U6hg3c{VG0t@Ar7(j)&O&*cL|nhpDWqV%eFeaL%hPqq?Eia^FH?_@^62`1M~c zu>aC}W~7MEiTj9bwMOg8HySTisOu6%H^^pBmTy|m#F0HmIkM*n3sx<#^iXiwbUt*^ zc0O^>dIlz(iRYYfzJlIS!GGN%`1(feAzf4+!1isscy9ByqWefoysG&l5-4KtI>Pop{0Z%i zucx}YisQ$Rvv1EHJUe&e-o6_@@U&**%(Kr)n+T^I#GDMnFnIcjCpq-(JHRmLS+|q= zwM$sKd^sH*9gK~Q^VIMDiagMp(O$^K6bG-i5K(T*oi4zn9(zb}?}JJd`D^$e~Gs_iRN- z!T};_d>^y*O%CPZ7ao!=Y$K%e!1w>ao4@-JG`o%221k63YXP>nbC6PIj{uwo*nML# zIf}G5mBcVeF_3d0=5*h4%FCQ)e~H91Izo+aEMt441XET@Hgj8SdT0{*sob&?=iy_hUSC43w*EmT4As5`wz2vyqIu|LN}0qT)RA3VsXDqjr{mGI{9xyMSuQC6;8$=tb}WzFSl7zW3V9b@1%!+&HIuu)aipwRTjSZ=AL98(HnZWnOStO8H{?vp!zcRL z_r%uR`zZX0v~})^1uo!MfhAm3v1dz`f${u5Ej1d>p|NA12xa&G5T4Ev#A zh=2V4L9To6wNX;GvOMQhd-(FpHu3YPm-3JO?dZBfc1yYNav>zE=Z^8AmmOkWqq$s2 zCd+uBe~6#{_*eLIh$zk#T6~Vf+Q>Po<`& zwxm{|l#mHhN383bkRjPoi0|s{v~ujnOh`HwnRf|`-1F5oNK)kgQGnE$Y!qk z(3^60kvsAHA%6Gq@8#Uy5C#EaIs9Jso>%8Z$U7f-4!_sW+1FkY^>8`s>I+l0(?dFc z`|0gz{j;YV6@y1;-2E`2v#vyOD-2E=1WT;0M9w^_6BGBh7-Ei_0MrygrBe_HNZ9=l z_MzNgru(yXIj#8+k?F4yxp8Nzn5P;yObk?Rc3CGl9IBd zt}@8$&N|N3XY}H*#d&|EY1ibWhaY|aHhO#dOWH3QfKUvi5wTJW*OP@XBlpq~v$K5; z4d!A=(&e+Zn-D#Cwi*x&y$Bw^7r39z)RkE1@^Ek zXH9}|#@qo`w+-{i!8ZQ7a~{WrE6lv?MUb*UQePS1^3}aub9xU=)sZ#wV(Wiewa4f4 z^RplQn&)@#D|w%2k%q|}tlM;m$reUN*`oEOm!ZUkQ41kxZ||V3wT6?%cU4r8k<2@ztol}=oa>|Uc%N<97`kpYaJU>9NLk>}bgo!_f{T`%WJh;B zj~r-Ydv`q}9=n;H%_2EbGAS#Hq|y;$N%J@tt>|at;vs6?p|~APqzjGqdVSpTli%>{ zGh51Y9R-tX;o8UJPV4!U0zwbr>fVp5 zdq1slze^$5xA8`N%g#)@uauH;Z-7aUpE+HRGk?#$sCsE8e$<{1c6d@~I5-Bwd#3z;T+NMxI| zix2!bcK8H8`|+{-zx*}x&)vxCRj1%|xsXzF z?ATF`-TEus$4`bQj3o#I#p^@q8DPPJ1!<3WvS$#*>reR2eyo=xxBQld))qSEb|Qqp z=ku~_*G~36`YfKuc4a>gOtMrkJuSi~mr|asjzS4LzW1adqv55wz|f5xw~QSvyow+a zGg2@gv9w=I)PiX^j&KV^h%gdRHGZTh- zhPdNLcXIZX8(DSMDm0sx6Jc8YRTU%7x}~5xB0lq*AK%KIzrTmRv7xe_zYro8IvtZO z3U`4s>q+lzp-yQ(5v&WZaUpmNi;XgkcKn*)UZL=T2xSn9}z zVcBXq3%AuXlGydRq^esoskmwwdZDKEm(uYt-FnqF~-h(G- zTiTg*TWJ{V{a1eamLPHP*nHpPoOAu9v@M#OapFo!$>Ht$+4X2{KCUk$2x)x}8B#Jf z>Br~wvE=!CX+QWVq2$~Qr8s^$&yDGm)BIjP58wVzx^^Gt+?St2Lt8_}3AWrU>B9Ey z>+a`oe|muDx9%mN2d8A95_fJ`E-Uq*nTW&y!?49&lKc=cBz0Xu$_Uq6*P~as@h)40 z2T1FSp@xr!Eoe^>}#v6*r>yTaR2p&Bo};Wdt;b84NP% zCm%$HmT?3@ivW>GdF;_lEstt_7W)#vZx+d-zN4AbUw%H$%JfU+N+~(9`w-jy_9W8C z&q~&j1j4VvVCG;x=ekR&sI5x7&uHHekN^2Wd=oP($YUxn(s?R}FTN>Zz%ry{WZXy4 z=VSTKztC~y2|_^h{h>t9bL3q%K=}SCAx}h-69v%8_dToBusiLXvT-%1o_{J;byaDD zGjjc#>o(&f<2?D$vpn_iGttEQhLnsM9)grF%G734lL%RYp=;!07bYV@riP(W46#Sr zBAsql-1>DoS0-j58dlw)wVy;c47;J`!lK>my$S< z1PPOUi3u4Xh#s@-ry1@lH!IdHXZ2aDsco)}W`=~)k+YPN!M-83Jid+1k8fc-ZZU{R z7QJ!9i@%Idh|Q)ZW_v4{w}p8c6fS#2v@rvGKDPYfPI|5!!yRWOdpsUa-unkx za%j(C{Au_^2!Se;SYTwCkf0$)Nw5fS3$rd6O@dM=%)!All^yp!#>qp+Fydan8 zzouc}K*DzA9I(r+BhNGh{Xx2RbQI?UCqml(sV%b6o%N7(= zPrQy?OjviwfMLUpPe!!yJlg?;fC+?UqM-zx*g~=ZQ?jspC%vWIZHF##& zMKd4L?j*3}bVdz3l}EPY_N2~b7~wt*LCGZx17acXC6+A=B>pn$rKN^u)6kqYY;HR? zmmSTfg<}>Zpr&WtSyCxP|uu~zFbgz`1HRUM8Jn**Ap~n+pb*4FXy>2yYAmmFhn2_AUHN&mW5g& zlINqmMpFXo#{8utWLo%%#oXVDM6vaVh-^{Bxlr8ApBtG^FXCO_UPS4T(4S@CiSm*C zba^8e+v@T`39kfRPFR}3#gb{{&E8yBzCR6lUGV*P~$+ zHOMIOU1D{NOvlgGzJgm-TD)i>mj~r0_xPV5!yX_4Ob zE$2%#)0Z%sb&ud|KzSzi$~|EiYx(TLZ1%LsNXVsdTPulCG|pB1(+togaQPl9_Ixs8pEW4e+9coY>xSwKBMC~9Cd=bw z2PvX403ycDSqUa;HTPZ{4(g4zrlpds<1nCXWSBL~t zNK8XEz}c2$DAwlkwYr?aAz2Aohs2DmnYb~f!AV$O{3F}A<;XrB)pwxp> z4Tva_YN~?b&`>lLA;8cjdN7FY4}@3hh%6ASqwAySatPfRM!`T4!d&|olB{Nvc*^a? zF`Edv+(Jt^C%&^=$VH~ntgiO4aPdHR@zlak!_3v3y$=707J`Fy(SJMVcB9peMgQ&7 zHF}3@*nOx2uiueZ8*)$PV%=ZqZtQa2@Dd2Ab`E#0r)&P1j8(N-Zx|nd6W8<@S1T9- zMr+y`t!d}rvWux2@1^6=6SN(E79|w;w;|_bA`T%jlzPzGK&dq+ery`<=33lMH8|=j zaa2{HI&Cp4SH|8vrEU=P`0$NR;vE`evVR!wkcMH@A*GIC3=uMV5z>Q#2!}Mw1Y$0S zrYTHotgO~%IUu6{FHf6eNrpnccTETv?^@Zpnzl2X29f_f{IchQpRL4_;8rDyHn z_xEqiD;a+=)0>IWe7RfD`9cCc&CA$&$~8<@G(;;;A>agZ&7FV~>HtE{N)E1lIo*rS zX6bW(rvBvK7o4>mv5E~Ox|Ha`jkGOBsMV-;8+A+Dsa-S&&1p|p0Wp)<{Kz;fIchP7 zw^KEZ+Kx7A+gtICPcm@e2qVW8gkFtd44@k)!jshDSLiZ!_-wqEqRe)^TBhZkaw6o| z#NG?bC##`mf`*!jysYGOu5XEo-t-};+S$JPWgMP&7Kp?LRm8JtR*8b_+a!^=uey=# z7rv9W15dJe%U{q!0bU4*kg<+Lm!NBNL7R)BY1A!gr(s1Ws@;~U0&=m6Ng(Lhza5no zw69%3B+NF~H?z&<_p}gY@U+OyLXJ-C z&Atnh`Me}9XMUt%TFBCTH=M%mNOY94#&|_NPpy9wW3}z!^NZV~KmyKR#t`%_-N<<3 zTvk2y3)~Z_>&(q2$S`>tQW6vz(!LC#*5YiarfuC)9JQ6%Dj=SWWm*DGBy1v{qH)_e zDk_+N##%;C_HttP9)i9F=tdvX=s^(@DFsr=VIS^4lS)C?Ujut%mB5i-`LK}fie_LT_LMg7vbG@h~mMO8BGOq7Tr?BYg70*<$s z6UaDeo0^tp+%+{E+p&}J0WfeNjUz~58VaQ*<=N=^R&JClS}!NJHxn;j4k-K*lvC0e zXqeAa8*e0NuSg--kEDD#5W5MRU@+ZYkCCS~B|t7a&pC zYzj5CPMVtCsES~8#7}ScIDW5gxsT1^VE$>R(Y*44SqT_GQDAAagIBGq=CT!) zwAb1)8si%yXCvXnXpo2ZPV(oiW9;qqW5k)y>8xnoRmI+m-^!}{e?-;D$&&R)IT4+} zlai3A0LMyHyPNinD{;5hTtiRCw)0tvd51ce=GUx8|K z(SG(S+%0wK2d^v=g`xgr949JgdELB@$XH>Ks2ni zloul3UNsbzE$d&6r?MfdCxy!{c;|U_y!GrlDjdaUb+)O3%U4u#?jkq8cxHs3J~50h zC@lwqNK*o}EgU@mwXAsH78Ii#X<14GDJ2Gy5CUmihN8J>-LM>YOKomsoJOLk3(FC~ zfX<$s1MJx`00LA+V6&^Z+!~EdZWb-8!(EZ+I@dn0nu}kykb8c=*HTlCka95Kr~lYt zq|t&ddr(Crim&)*b~Zls<#v5j%HaBzhh#CE#>euaB#G+NOx)#q107s;J_D^w(z>8U zFsDZ2f3I(4{d`x+`@wBjc=x&WY?$ZbBY)|oXDk#u2@4rxymJNJtIwxn`@s<-QORiT4g}F;sVt2YwX~94<6GfF^@@MwZaq;qEHk4yxFK0jG zgh-=*b3{k60^HMzg+#Dt?UjV=PV;DI$az)>*XxH|JD+VKE)v-Rp=;STY%2uu5nOP>!7CINp+2bs%kq9rp9K}2*^mr<@7}!YFR{o`|{}G0am0}t@_?kgHYTwu3muM z?Xcd^LZYayb}(;#4RhKnsH=BTS!u&=SJ7+=Hk*pgrl8rBf+yLI9~xz3z?H1b)N-l?MT^?Q*@S>-Mi9*N4VtY&ihgMyPRMlj+ z-{=7pA^7i?HnFtXUceEG$C5@nAG@THPya10p;Pi?pQ6!w`XyA~b8tr7MP|Yvi7srQ zwc)I*qGob4chHNT7O}3#sjRfod=qtZ7;IgfFp-z+ z>UQB0H&?B$nwDhdW|IUjTVBZn`zLv{%ab;UWMbY@y!CCkhh~OvvLPi>Yefh<4J$fP zG&NRe5lt8QY1#4yF1Wa}p#GMIBoNT?`*Z^S5W#>!z;AvJ_;tod{T$vq#G&Vh2>Qbh zy^Na>w21JF{M+HIprL6FeJ2ALxQe%uF*Q?THrdFL93PZ{lEamj+VxG#NAQ0kW_>H9 zE6e>Vln&jC&OmCmw7Rj=Ciw8BjVMa?)*T!0@U6S{v+eLGOWG=V#}x}Wbz!Z!JWY{M z6u}42tLK^HUIL*s?r?S+L(A9We&R3F(33GpBHFPEY736qN-Emw<2OhkyJSD$Iqj7> zk+ES&#zy`0o|vHL#5e=J9tL|oOily{_(BAHI!4q2PCQ0_Nr`0C6|SkZlY!o27bV-RE4mZ^`jSLM1kZ710oC}vAW}|dbcGtHO z_KDB$N6~X^XDkwI_Kc9_f}m7Oujk&E6LoEPtLjtT+Z6MoDDbBBwK-X9PwXA$gj$2M zz81~pz)@3;qobXBx^!Og)o1y}9s3xXC@yuT5Q5jOtwB*loGY9#2bAzRglZe`HnbIW zoKjo?LrPGaQ0+FV+MBG&Fu{c_X2~Ay8sXU|PNc1Wpx49iZ{5L>10#uKER)DM<$I!g zn@rhGT7HjrrxOV3o7%%Gf4CEq8~vN0+(U>WN@#`4e3z3JlrKPhmLXlo?TOC4AB<{PXeL;z@Odg1Es!|1LRJJw5*3!*`G($myzx-h@ zo1f}V3BstUbMo4^okB}nWg3D{ALt=-ICjJ;PS;jd)u^nf1!<42j8iBrPuJW@F(`L? zORZBE5C#0gvXBey%2=ZMa<)!J|L>`5Ng&_^OH;u5&U~WrQtL?kE>InIs^+$G)LzR? zx9#EW-`&ivqhonTeVsnXX>pv`KoDqZoz^=_N(o98DBxH~z=zy!85|)Hk|vAX#Z)6aLYLrZY^Z$fu6%BD*G7nxk9+ zapW6ck7c$vfxXg&t)hcXqZ3?z^K)ExdL!?B*&^ENa~lAbG}y6ef>8W=lX1s3Dk}-O zDzSMd3c3!(Aj)A%B{ruWo6C`Lox5Zgvt%-qy20irPSCY)h)b_p#M0Fz*vwiy}TKC3Ty`e^*x2h70US=`Z-PjZyL}GRg#HS!&6w+f4AI z3Z-POZv{veFRYn?Vl3pT8jeB^H-;l-X_92owpM94)I2Vwg>(>&{7aBR%$yX8B5>AM zQL%U~w;%L#<(Hn~XMgX?Nycz!3N4kYrCcEd7!D`8vv{wLLSZ04II+2%G4G0OmZ=13 zkanA)J`aDqZ709^;Z}|u7*2SraJn>3+c<}}e`EvKzil;h=7raD%tfLwlE^s62|>H& zL{&8;c8Yy=XPXHoN-ioS_J#R4mQKtmeQ42aLpD=4!*03lVO@7VW458HvFJPt_py4n zLOy&9aj#lwe43M8>Hu?@`?h+FrW*eDpS@_B!i^Wt%Xl1BK%GW2VqPR zA`^P%BcNfcbY-+gBNlS11slIj?B6mZIkb0(!~2Jsx2Tql7k9E~c|$aDkj<{L>Wo&F zt!?Je?m-^E|1d}P4`;T@qc5kKE%#)S))X6>W+xa@kwx;xth11(07?mar2xqSUN~|^ z#NyAt3nZCeFG|`wVQV$M@_6EO*%fk}&x=%NYJye-oQ0qPRJ%saf)@UE(8HT9kf>_< zgDc_o#i9%2{zOuS=F|8RLkbm%g3aYfwKSn1GK`l!jF3Uso*@qJ9iq9dk~JGzS-!TJ zx+WJw2sBM$>6#`Mt!m)NzG0sD+hGpw8i<8S#~mZL1yU3LEu=s}#irWvqDD6>CauM6 zL)m?ig;FwEBrifv3<-W=pqPwf#U!fFS7Wv&H1m5QH(FGW&dEZy8Vm&X+RCg)j+C3n z6Dh}u`X^5lBq|6{ZCa`XK#_mcR&8?L*wz#Qna8g)AN10bB#4 z{5PA{*UK}PMcm&P2qz0Glm~KZdqj)Qi-1WP z3@%<=l~pQ{61+h@M!HF1)X^8NX@GfH1&|0;!OB8TO(KuAbqch!SF&PF6N^_i(9~K{ zP7t?{Ve?{q_wVj<@uQ7A&N^0a2oys(3|j%v%~^4Z|?1UY_igrob1>DoO+&(Sf0 ze%+KUz=D7?OB7Md7!m!AxJ`?_L1&{qR$fvTrCCLYLMQb~y|UjlLJ|4e#&O+1=9@EH zSl&V<$hbF(~!-w#DJ0jH zghNSj*AoJS4o0r=`{jTz13;v%Cz5dv*66&&wY>DYr8Kow3QNHYHWkxwCBTJcvvp@?ua%!`G za!fMb{ri?aoEgy6Wv5imm$MI4;newS4x<2NeHAw4}{`e&oWCCnx=5YE0=KE#yL4< z)-!jL96vP5j;FfWv$dbGAz$p3r${df(;6ltDThLV$i@`<%tn;b+2&~>!v4bLXmzCS zQl;V4hJ&HJFOM_?h5@-;SX*csGkN8J2|dWD?jtDkADcqJgitWjbb~n`V_hi2`=`^7 z4I&p;5Da**8wOqxZ?4Amz9QIk%#$-ACmc45j@;x{dE2SAT(PJETLIn>n@@U7FN!qB zF}ymypcfJH=3RHOFr*}ifo9VQ`h2NTZx&?ea%sH&U8gdqGq;@6(va-k-p?a3#B?8=OcE)7B$lx66b5?uz- gjbKr|64HbGf2T}w&*?T@KmY&$07*qoM6N<$f_tT)#{d8T literal 0 HcmV?d00001 diff --git a/searchd/meson.build b/searchd/meson.build new file mode 100644 index 000000000..d4ee982d4 --- /dev/null +++ b/searchd/meson.build @@ -0,0 +1,15 @@ +if not get_option('searchd') + subdir_done() +endif + +searchd_args = ['-DG_LOG_USE_STRUCTURED', '-DG_LOG_DOMAIN="mobi.phosh.Shell.Search"'] + +executable( + 'phosh-searchd', + ['searchd.c', 'search-provider.c'], + include_directories: [root_inc], + dependencies: phosh_search_dep, + c_args: searchd_args, + install_dir: libexecdir, + install: true, +) diff --git a/searchd/search-provider.c b/searchd/search-provider.c new file mode 100644 index 000000000..3f26ccea8 --- /dev/null +++ b/searchd/search-provider.c @@ -0,0 +1,684 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +/* + * Based on search.js / remoteSearch.js: + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/2d2824b947754abf0ddadd9c1ba9b9f16b0745d3/js/ui/search.js + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/0a7e717e0e125248bace65e170a95ae12e3cdf38/js/ui/remoteSearch.js + * + */ + +#include + +#include "search-provider.h" +#include "search-result-meta.h" +#include "gnome-shell-search-provider.h" + +/** + * PhoshSearchProvider: + * + * Provider for handling user-initiated searches + * + * The #PhoshSearchProvider class is handle searches initiated by the + * user. It interfaces with the D-Bus to communicate with the search service, + * allowing for the retrieval and activation of search results. + */ + + +typedef struct _PhoshSearchProviderPrivate PhoshSearchProviderPrivate; +struct _PhoshSearchProviderPrivate { + GAppInfo *info; + PhoshDBusSearchProvider2 *proxy; + GCancellable *cancellable; + GCancellable *parent_cancellable; + gulong parent_cancellable_handler; + GDBusProxyFlags proxy_flags; + char *bus_name; + char *bus_path; + gboolean autostart; + gboolean default_disabled; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSearchProvider, phosh_search_provider, G_TYPE_OBJECT) + +enum { + PROP_0, + PROP_APP_INFO, + PROP_BUS_NAME, + PROP_BUS_PATH, + PROP_AUTOSTART, + PROP_DEFAULT_DISABLED, + PROP_PARENT_CANCELLABLE, + LAST_PROP +}; +static GParamSpec *pspecs[LAST_PROP] = { NULL, }; + +enum { + READY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +static void +got_proxy (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (PhoshSearchProvider) self = PHOSH_SEARCH_PROVIDER (user_data); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->proxy = phosh_dbus_search_provider2_proxy_new_for_bus_finish (res, &error); + + if (!priv->proxy) { + g_warning ("[%s]: Unable to create proxy: %s", priv->bus_path, error->message); + return; + } + + g_debug ("[%s]: Got proxy", priv->bus_path); + + g_signal_emit (self, signals[READY], 0); +} + + +static void +phosh_search_provider_constructed (GObject *object) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + G_OBJECT_CLASS (phosh_search_provider_parent_class)->constructed (object); + + phosh_dbus_search_provider2_proxy_new_for_bus (G_BUS_TYPE_SESSION, + priv->proxy_flags, + priv->bus_name, + priv->bus_path, + priv->cancellable, + got_proxy, + g_object_ref (self)); +} + + +static void +phosh_search_provider_finalize (GObject *object) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + + g_cancellable_disconnect (priv->parent_cancellable, + priv->parent_cancellable_handler); + + g_clear_object (&priv->info); + g_clear_object (&priv->proxy); + g_clear_object (&priv->parent_cancellable); + + g_clear_pointer (&priv->bus_name, g_free); + g_clear_pointer (&priv->bus_path, g_free); + + G_OBJECT_CLASS (phosh_search_provider_parent_class)->finalize (object); +} + + +static void +parent_canceled (GCancellable *cancellable, PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + g_debug ("Provider %s cancelling", priv->bus_name); + + g_cancellable_cancel (priv->cancellable); + g_clear_object (&priv->cancellable); + priv->cancellable = g_cancellable_new (); +} + + +static void +set_parent_cancellable (PhoshSearchProvider *self, GCancellable *parent_cancellable) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + priv->parent_cancellable = parent_cancellable; + + if (priv->parent_cancellable) { + priv->parent_cancellable_handler = g_cancellable_connect (priv->parent_cancellable, + G_CALLBACK (parent_canceled), + self, + NULL); + } +} + + +static void +set_autostart (PhoshSearchProvider *self, gboolean autostart) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + priv->autostart = autostart; + if (priv->autostart) { + /* Delay autostart to avoid spawing everything at once */ + priv->proxy_flags |= G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION; + } else { + /* Don't attempt to autostart */ + priv->proxy_flags |= G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START; + } +} + + +static void +phosh_search_provider_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + priv->info = g_value_dup_object (value); + break; + case PROP_PARENT_CANCELLABLE: + set_parent_cancellable (self, g_value_dup_object (value)); + break; + case PROP_BUS_NAME: + priv->bus_name = g_value_dup_string (value); + break; + case PROP_BUS_PATH: + priv->bus_path = g_value_dup_string (value); + break; + case PROP_AUTOSTART: + set_autostart (self, g_value_get_boolean (value)); + break; + case PROP_DEFAULT_DISABLED: + priv->default_disabled = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_provider_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (object); + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + g_value_set_object (value, priv->info); + break; + case PROP_PARENT_CANCELLABLE: + g_value_set_object (value, priv->parent_cancellable); + break; + case PROP_BUS_NAME: + g_value_set_string (value, priv->bus_name); + break; + case PROP_BUS_PATH: + g_value_set_string (value, priv->bus_path); + break; + case PROP_AUTOSTART: + g_value_set_boolean (value, priv->autostart); + break; + case PROP_DEFAULT_DISABLED: + g_value_set_boolean (value, priv->default_disabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_search_provider_class_init (PhoshSearchProviderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_search_provider_constructed; + object_class->finalize = phosh_search_provider_finalize; + object_class->set_property = phosh_search_provider_set_property; + object_class->get_property = phosh_search_provider_get_property; + + pspecs[PROP_APP_INFO] = + g_param_spec_object ("app-info", "", "", + G_TYPE_APP_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_PARENT_CANCELLABLE] = + g_param_spec_object ("parent-cancellable", "", "", + G_TYPE_CANCELLABLE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_BUS_NAME] = + g_param_spec_string ("bus-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_BUS_PATH] = + g_param_spec_string ("bus-path", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_AUTOSTART] = + g_param_spec_boolean ("autostart", "", "", + TRUE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + pspecs[PROP_DEFAULT_DISABLED] = + g_param_spec_boolean ("default-disabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, pspecs); + + signals[READY] = g_signal_new ("ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + + +static void +phosh_search_provider_init (PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + priv->cancellable = g_cancellable_new (); + priv->proxy_flags = G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES; + priv->autostart = TRUE; +} + + +PhoshSearchProvider * +phosh_search_provider_new (const char *desktop_app_id, + GCancellable *parent_cancellable, + const char *bus_path, + const char *bus_name, + gboolean autostart, + gboolean default_disabled) +{ + return g_object_new (PHOSH_TYPE_SEARCH_PROVIDER, + "app-info", g_desktop_app_info_new (desktop_app_id), + "parent-cancellable", parent_cancellable, + "bus-path", bus_path, + "bus-name", bus_name, + "autostart", autostart, + "default-disabled", default_disabled, + NULL); +} + + +GPtrArray * +phosh_search_provider_limit_results (GStrv results, int max) +{ + g_autoptr (GPtrArray) normal = NULL; + g_autoptr (GPtrArray) special = NULL; + GPtrArray *array; + int i = 0; + + g_return_val_if_fail (results != NULL, NULL); + + normal = g_ptr_array_new_full (max * 1.5, g_free); + special = g_ptr_array_new_full (max * 1.5, g_free); + array = g_ptr_array_new_full (max * 1.5, g_free); + + while (results[i]) { + if (G_UNLIKELY (g_str_has_prefix (results[i], "special:"))) + g_ptr_array_add (special, g_strdup (results[i])); + else + g_ptr_array_add (normal, g_strdup (results[i])); + + if (special->len == max && normal->len == max) + break; + + i++; + } + + for (i = 0; i < max && i < normal->len; i++) + g_ptr_array_add (array, g_ptr_array_steal_index (normal, i)); + + for (i = 0; i < max && i < special->len; i++) + g_ptr_array_add (array, g_ptr_array_steal_index (special, i)); + + return array; +} + + +static void +result_activated (GObject *source, GAsyncResult *res, gpointer data) +{ + g_autoptr (GError) error = NULL; + gboolean success; + + success = phosh_dbus_search_provider2_call_activate_result_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + res, + &error); + + if (!success) + g_warning ("Failed to activate result: %s", error->message); +} + + +void +phosh_search_provider_activate_result (PhoshSearchProvider *self, + const char *result, + const char *const *terms, + guint timestamp) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + phosh_dbus_search_provider2_call_activate_result (priv->proxy, + result, + terms, + timestamp, + priv->cancellable, + result_activated, + NULL); +} + + +static void +search_launched (GObject *source, GAsyncResult *res, gpointer data) +{ + g_autoptr (GError) error = NULL; + gboolean success; + + success = phosh_dbus_search_provider2_call_launch_search_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + res, + &error); + + if (!success) + g_warning ("Failed to launch search: %s", error->message); +} + + +void +phosh_search_provider_launch_search (PhoshSearchProvider *self, + const char *const *terms, + guint timestamp) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + + phosh_dbus_search_provider2_call_launch_search (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + terms, + timestamp, + priv->cancellable, + search_launched, + NULL); +} + + +static GIcon * +get_result_icon (PhoshSearchProvider *self, GVariantDict *result_meta) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) variant = NULL; + g_autofree char *icon_data = NULL; + GIcon *icon = NULL; + int width = 0; + int height = 0; + int row_stride = 0; + int has_alpha = 0; + int sample_size = 0; + int channels = 0; + + if (g_variant_dict_lookup (result_meta, "icon", "v", &variant)) { + icon = g_icon_deserialize (variant); + } else if (g_variant_dict_lookup (result_meta, "gicon", "s", &icon_data)) { + icon = g_icon_new_for_string (icon_data, &error); + + if (error) { + g_warning ("[%s]: bad icon: %s", priv->bus_path, error->message); + } + } else if (g_variant_dict_lookup (result_meta, "icon-data", "iiibiiay", + &width, &height, &row_stride, &has_alpha, + &sample_size, &channels, &icon_data)) { + icon = G_ICON (gdk_pixbuf_new_from_data ((guchar *) icon_data, + GDK_COLORSPACE_RGB, + has_alpha, + sample_size, + width, + height, + row_stride, + (GdkPixbufDestroyNotify) g_free, + NULL)); + } + + return icon; +} + + +static void +got_result_meta (GObject *source, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) metas = NULL; + g_autoptr (GPtrArray) results = NULL; + g_autoptr (GTask) task = user_data; + g_autofree char *bus_path = NULL; + GVariant *val = NULL; + GVariantIter iter; + gboolean success; + + PhoshSearchProvider *self = PHOSH_SEARCH_PROVIDER (g_task_get_source_object (task)); + + g_object_get (self, "bus-path", &bus_path, NULL); + + success = phosh_dbus_search_provider2_call_get_result_metas_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &metas, + res, + &error); + + if (!success) + g_warning ("[%s]: Failed get result meta: %s", bus_path, error->message); + + results = g_ptr_array_new_full (100, (GDestroyNotify) phosh_search_result_meta_unref); + +/* + * Some providers decide to provide NULL instead of an empty array + * thus we do what JS does and map NULL to an empty array + */ + if (metas == NULL) + metas = g_variant_new ("aa{sv}", NULL); + + g_variant_iter_init (&iter, metas); + while (g_variant_iter_loop (&iter, "@a{sv}", &val)) { + g_auto (GVariantDict) dict; + g_autofree char *id = NULL; + g_autofree char *name = NULL; + g_autofree char *desc = NULL; + g_autofree char *clipboard = NULL; + g_autoptr (GIcon) icon = NULL; + PhoshSearchResultMeta *meta; + + g_variant_dict_init (&dict, val); + + if (!g_variant_dict_lookup (&dict, "id", "s", &id)) { + g_warning ("Result is missing a result id"); + continue; + } + + if (!g_variant_dict_lookup (&dict, "name", "s", &name)) { + g_warning ("Result %s is missing a name!", id); + continue; + } + + g_variant_dict_lookup (&dict, "description", "s", &desc); + + /* Isn't consistent naming great? */ + g_variant_dict_lookup (&dict, "clipboardText", "s", &clipboard); + + icon = get_result_icon (self, &dict); + + meta = phosh_search_result_meta_new (id, name, desc, icon, clipboard); + + g_ptr_array_add (results, meta); + } + + g_task_return_pointer (task, g_ptr_array_ref (results), (GDestroyNotify) g_ptr_array_unref); +} + + +void +phosh_search_provider_get_result_meta (PhoshSearchProvider *self, + GStrv results, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + + g_task_set_source_tag (task, phosh_search_provider_get_result_meta); + + phosh_dbus_search_provider2_call_get_result_metas (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + (const char * const*) results, + priv->cancellable, + got_result_meta, + task); +} + + +GPtrArray * +phosh_search_provider_get_result_meta_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_provider_get_result_meta); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +static void +got_results (GObject *source, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GTask) task = user_data; + GStrv results = NULL; + + if (g_task_get_source_tag (task) == phosh_search_provider_get_initial) { + phosh_dbus_search_provider2_call_get_initial_result_set_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &results, + res, + &error); + } else { + phosh_dbus_search_provider2_call_get_subsearch_result_set_finish (PHOSH_DBUS_SEARCH_PROVIDER2 (source), + &results, + res, + &error); + } + + if (error) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_pointer (task, results, (GDestroyNotify) g_strfreev); +} + + +void +phosh_search_provider_get_initial (PhoshSearchProvider *self, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_provider_get_initial); + + phosh_dbus_search_provider2_call_get_initial_result_set (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + terms, + priv->cancellable, + got_results, + task); +} + + +GStrv +phosh_search_provider_get_initial_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_provider_get_initial); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +void +phosh_search_provider_get_subsearch (PhoshSearchProvider *self, + const char *const *results, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchProviderPrivate *priv = phosh_search_provider_get_instance_private (self); + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_provider_get_subsearch); + + phosh_dbus_search_provider2_call_get_subsearch_result_set (PHOSH_DBUS_SEARCH_PROVIDER2 (priv->proxy), + results, + terms, + priv->cancellable, + got_results, + task); +} + + +GStrv +phosh_search_provider_get_subsearch_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_provider_get_subsearch); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +gboolean +phosh_search_provider_get_ready (PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SEARCH_PROVIDER (self), FALSE); + + priv = phosh_search_provider_get_instance_private (self); + + return priv->proxy != NULL; +} + + +const char * +phosh_search_provider_get_bus_path (PhoshSearchProvider *self) +{ + PhoshSearchProviderPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SEARCH_PROVIDER (self), FALSE); + + priv = phosh_search_provider_get_instance_private (self); + + return priv->bus_path; +} diff --git a/searchd/search-provider.h b/searchd/search-provider.h new file mode 100644 index 000000000..0f8925765 --- /dev/null +++ b/searchd/search-provider.h @@ -0,0 +1,64 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_PROVIDER phosh_search_provider_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchProvider, phosh_search_provider, PHOSH, SEARCH_PROVIDER, GObject) + +struct _PhoshSearchProviderClass +{ + GObjectClass parent_class; +}; + +PhoshSearchProvider *phosh_search_provider_new (const char *desktop_app_id, + GCancellable *parent_cancellable, + const char *bus_path, + const char *bus_name, + gboolean autostart, + gboolean default_disabled); +GPtrArray *phosh_search_provider_limit_results (GStrv results, + int max); +void phosh_search_provider_activate_result (PhoshSearchProvider *self, + const char *result, + const char *const *terms, + guint timestamp); +void phosh_search_provider_launch_search (PhoshSearchProvider *self, + const char *const *terms, + guint timestamp); +void phosh_search_provider_get_result_meta (PhoshSearchProvider *self, + GStrv results, + GAsyncReadyCallback callback, + gpointer callback_data); +GPtrArray *phosh_search_provider_get_result_meta_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +void phosh_search_provider_get_initial (PhoshSearchProvider *self, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data); +GStrv phosh_search_provider_get_initial_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +void phosh_search_provider_get_subsearch (PhoshSearchProvider *self, + const char *const *results, + const char *const *terms, + GAsyncReadyCallback callback, + gpointer callback_data); +GStrv phosh_search_provider_get_subsearch_finish (PhoshSearchProvider *self, + GAsyncResult *res, + GError **error); +gboolean phosh_search_provider_get_ready (PhoshSearchProvider *self); +const char *phosh_search_provider_get_bus_path (PhoshSearchProvider *self); + +G_END_DECLS diff --git a/searchd/searchd.c b/searchd/searchd.c new file mode 100644 index 000000000..c170f8fd6 --- /dev/null +++ b/searchd/searchd.c @@ -0,0 +1,900 @@ +/* + * Copyright (C) 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Zander Brown + * Guido Günther + * Gotam Gorabh + */ + +/* + * Based on gnome-shell's original js implementation + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/2d2824b947754abf0ddadd9c1ba9b9f16b0745d3/js/ui/search.js + * https://gitlab.gnome.org/GNOME/gnome-shell/blob/0a7e717e0e125248bace65e170a95ae12e3cdf38/js/ui/remoteSearch.js + * + */ + +#include "phosh-searchd.h" +#include "searchd.h" +#include "search-provider.h" + +#include + +#include +#include + +#define GROUP_NAME "Shell Search Provider" +#define SEARCH_PROVIDERS_SCHEMA "org.gnome.desktop.search-providers" + +#define LIMIT_RESULTS 5 + +/** + * PhoshSearchApplication: + * + * The #PhoshSearchApplication class that serves as a service to facilitate search + * operations within the Phosh desktop environment. It interacts with various search + * providers to perform queries and return results. + */ + + +typedef struct _PhoshSearchApplicationPrivate PhoshSearchApplicationPrivate; +struct _PhoshSearchApplicationPrivate { + PhoshDBusSearch *object; + + GSettings *settings; + + /* element-type: Phosh.SearchSource */ + GList *sources; + /* element-type: Phosh.SearchProvider */ + GHashTable *providers; + + /* key: char * (object path), value: GVariant * (results) */ + GHashTable *last_results; + gboolean doing_subsearch; + + char *query; + GStrv query_parts; + + GCancellable *cancellable; + + gulong search_timeout; + int outstanding_searches; + + GRegex *splitter; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSearchApplication, phosh_search_application, G_TYPE_APPLICATION) + + +static void +phosh_search_application_finalize (GObject *object) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (object); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + g_clear_object (&priv->object); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->cancellable); + g_clear_object (&priv->settings); + g_clear_pointer (&priv->last_results, g_hash_table_destroy); + + g_clear_pointer (&priv->query, g_free); + g_clear_pointer (&priv->query_parts, g_strfreev); + + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + g_clear_pointer (&priv->providers, g_hash_table_destroy); + + g_clear_pointer (&priv->splitter, g_regex_unref); + + G_OBJECT_CLASS (phosh_search_application_parent_class)->finalize (object); +} + + +static gboolean +phosh_search_application_dbus_register (GApplication *app, + GDBusConnection *connection, + const char *object_path, + GError **error) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (app); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (priv->object), + connection, + object_path, + error)) { + g_clear_object (&priv->object); + return FALSE; + } + + return G_APPLICATION_CLASS (phosh_search_application_parent_class)->dbus_register (app, + connection, + object_path, + error); +} + + +static void +phosh_search_application_dbus_unregister (GApplication *app, + GDBusConnection *connection, + const char *object_path) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (app); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + if (priv->object) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (priv->object)); + + g_clear_object (&priv->object); + } + + G_APPLICATION_CLASS (phosh_search_application_parent_class)->dbus_unregister (app, + connection, + object_path); +} + + +static void +phosh_search_application_class_init (PhoshSearchApplicationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GApplicationClass *app_class = G_APPLICATION_CLASS (klass); + + object_class->finalize = phosh_search_application_finalize; + + app_class->dbus_register = phosh_search_application_dbus_register; + app_class->dbus_unregister = phosh_search_application_dbus_unregister; +} + + +static gboolean +launch_source (PhoshDBusSearch *interface, + GDBusMethodInvocation *invocation, + const char *source_id, + guint timestamp, + gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GHashTableIter iter; + gpointer key, value; + + g_debug ("[LaunchSearch] Launching full search in source '%s' at timestamp %u", + source_id, timestamp); + + g_hash_table_iter_init (&iter, priv->providers); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PhoshSearchProvider *current_provider = PHOSH_SEARCH_PROVIDER (value); + const char *bus_path = phosh_search_provider_get_bus_path (current_provider); + + if (!phosh_search_provider_get_ready (current_provider)) { + g_warning ("[%s]: not ready", bus_path); + continue; + } + + if (g_strcmp0 (source_id, bus_path) == 0) { + g_debug ("Matched source_id '%s' with provider '%s'", source_id, bus_path); + + if (!priv->query_parts) { + g_warning ("[LaunchSearch] Cannot launch source '%s' — no search terms provided yet", source_id); + break; + } + + phosh_search_provider_launch_search (current_provider, + (const char * const*) priv->query_parts, + timestamp); + + break; + } + } + + phosh_dbus_search_complete_launch_source (interface, invocation); + + return TRUE; +} + + +static gboolean +activate_result (PhoshDBusSearch *interface, + GDBusMethodInvocation *invocation, + const char *source_id, + const char *result_id, + guint timestamp, + gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GHashTableIter iter; + gpointer key, value; + + g_debug ("[ActivateResult] Activating result '%s' from source '%s' at timestamp %u", + result_id, source_id, timestamp); + + g_hash_table_iter_init (&iter, priv->providers); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PhoshSearchProvider *current_provider = PHOSH_SEARCH_PROVIDER (value); + const char *bus_path = phosh_search_provider_get_bus_path (current_provider); + + if (!phosh_search_provider_get_ready (current_provider)) { + g_warning ("[%s]: not ready", bus_path); + continue; + } + + if (g_strcmp0 (source_id, bus_path) == 0) { + g_debug ("Matched source_id '%s' with provider '%s'", source_id, bus_path); + + if (!priv->query_parts) { + g_warning ("[ActivateResult] Cannot activate result '%s' — no search terms available", result_id); + break; + } + + phosh_search_provider_activate_result (current_provider, + result_id, + (const char * const*) priv->query_parts, + timestamp); + + break; + } + } + + phosh_dbus_search_complete_activate_result (interface, invocation); + + return TRUE; +} + + +static gboolean +get_sources (PhoshDBusSearch *interface, GDBusMethodInvocation *invocation, gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GVariantBuilder builder; + g_autoptr (GVariant) result = NULL; + g_autoptr (GList) list = NULL; + + list = priv->sources; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ssu)")); + + while (list) { + g_variant_builder_add_value (&builder, phosh_search_source_serialise (list->data)); + + list = g_list_next (list); + } + + result = g_variant_builder_end (&builder); + + phosh_dbus_search_complete_get_sources (interface, invocation, g_variant_ref (result)); + + return TRUE; +} + + +static gboolean +get_last_results (PhoshDBusSearch *interface, GDBusMethodInvocation *invocation, gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GVariantBuilder builder; + GHashTableIter iter; + gpointer key, value; + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a{saa{sv}}")); + + g_hash_table_iter_init (&iter, priv->last_results); + while (g_hash_table_iter_next (&iter, &key, &value)) { + const char *source_id = key; + GVariant *results = value; + GVariantIter result_iter; + GVariantBuilder results_array; + GVariant *result; + + g_variant_builder_init (&results_array, G_VARIANT_TYPE ("aa{sv}")); + g_variant_iter_init (&result_iter, results); + + while (g_variant_iter_next (&result_iter, "@a{sv}", &result)) + g_variant_builder_add_value (&results_array, result); + + g_variant_builder_add (&builder, "{saa{sv}}", source_id, &results_array); + } + + phosh_dbus_search_complete_get_last_results (interface, + invocation, + g_variant_builder_end (&builder)); + + return TRUE; +} + + +static void +got_metas (GObject *source, GAsyncResult *res, gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) result = NULL; + GVariantBuilder builder; + GPtrArray *metas; + char *bus_path; + + metas = phosh_search_provider_get_result_meta_finish (PHOSH_SEARCH_PROVIDER (source), + res, + &error); + + g_object_get (source, "bus-path", &bus_path, NULL); + + if (error) { + g_critical ("Failed to load results %s", error->message); + return; + } + + g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); + + for (int i = 0; i < metas->len; i++) { + g_variant_builder_add_value (&builder, + phosh_search_result_meta_serialise (g_ptr_array_index (metas, i))); + } + + result = g_variant_builder_end (&builder); + + phosh_dbus_search_emit_source_results_changed (priv->object, bus_path, g_variant_ref (result)); + + g_hash_table_insert (priv->last_results, bus_path, g_variant_ref (result)); +} + + +struct GotResultsData { + gboolean initial; + PhoshSearchApplication *self; +}; + + +static void +got_results (GObject *source, GAsyncResult *res, gpointer user_data) +{ + PhoshSearchApplicationPrivate *priv; + struct GotResultsData *data = user_data; + g_autoptr (GError) error = NULL; + GStrv results = NULL; + g_autoptr (GPtrArray) sub_res = NULL; + g_autofree char *bus_path = NULL; + GStrv sub_res_strv = NULL; + + priv = phosh_search_application_get_instance_private (data->self); + + g_object_get (source, "bus-path", &bus_path, NULL); + + if (data->initial) { + results = phosh_search_provider_get_initial_finish (PHOSH_SEARCH_PROVIDER (source), + res, + &error); + } else { + results = phosh_search_provider_get_subsearch_finish (PHOSH_SEARCH_PROVIDER (source), + res, + &error); + } + + if (error) { + g_warning ("[%s]: %s", bus_path, error->message); + } else if (results) { + sub_res = phosh_search_provider_limit_results (results, LIMIT_RESULTS); + + sub_res_strv = g_new (char *, sub_res->len + 1); + + for (int i = 0; i < sub_res->len; i++) + sub_res_strv[i] = g_ptr_array_index (sub_res, i); + + sub_res_strv[sub_res->len] = NULL; + + phosh_search_provider_get_result_meta (PHOSH_SEARCH_PROVIDER (source), + sub_res_strv, + got_metas, + data->self); + } + + priv->outstanding_searches--; + + /* If all searches are done, emit the signal */ + if (priv->outstanding_searches == 0) { + g_debug ("Query finished: All outstanding searches completed.\n"); + phosh_dbus_search_emit_query_finished (priv->object); + } + + g_object_unref (data->self); + g_free (data); +} + + +/* Returns newly allocated GStrv (NULL-terminated) containing the "id" strings + * extracted from the variant. + */ +static GStrv +extract_result_ids (GVariant *results_variant) +{ + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + GVariant *element; + GVariantIter iter; + + if (!results_variant) + return NULL; + + g_variant_iter_init (&iter, results_variant); + while (g_variant_iter_next (&iter, "@a{sv}", &element)) { + const char *id; + if (g_variant_lookup (element, "id", "&s", &id)) + g_strv_builder_add (builder, id); + + g_variant_unref (element); + } + + return g_strv_builder_end (builder); +} + + +static void +search (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, priv->providers); + while (g_hash_table_iter_next (&iter, &key, &value)) { + PhoshSearchProvider *provider = PHOSH_SEARCH_PROVIDER (value); + char *bus_path; + struct GotResultsData *data; + + g_object_get (provider, "bus-path", &bus_path, NULL); + + if (!phosh_search_provider_get_ready (provider)) { + g_warning ("[%s]: not ready", bus_path); + continue; + } + + data = g_new (struct GotResultsData, 1); + data->self = g_object_ref (self); + + /* Increment counter for each provider that will be queried */ + priv->outstanding_searches++; + + if (priv->doing_subsearch && g_hash_table_contains (priv->last_results, bus_path)) { + GVariant *prev = g_hash_table_lookup (priv->last_results, bus_path); + g_auto (GStrv) prev_results = extract_result_ids (prev); + + data->initial = FALSE; + phosh_search_provider_get_subsearch (provider, + (const char * const *) prev_results, + (const char * const *) priv->query_parts, + got_results, + data); + } else { + data->initial = TRUE; + phosh_search_provider_get_initial (provider, + (const char * const*) priv->query_parts, + got_results, + data); + } + } + + g_hash_table_remove_all (priv->last_results); + + if (priv->search_timeout != 0) { + g_source_remove (priv->search_timeout); + priv->search_timeout = 0; + } +} + + +static gboolean +search_timeout (gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + + priv->search_timeout = 0; + + search (self); + + /* Edge case: if no providers are ready/active, emit immediately */ + if (priv->outstanding_searches == 0) + phosh_dbus_search_emit_query_finished (priv->object); + + return G_SOURCE_REMOVE; +} + + +static gboolean +query (PhoshDBusSearch *interface, + GDBusMethodInvocation *invocation, + const char *query, + gpointer user_data) +{ + PhoshSearchApplication *self = PHOSH_SEARCH_APPLICATION (user_data); + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + g_autofree char *striped = NULL; + g_auto (GStrv) parts = NULL; + int len = 0; + + striped = g_strstrip (g_strdup (query)); + parts = g_regex_split (priv->splitter, striped, 0); + + len = parts ? g_strv_length (parts) : 0; + if (priv->query_parts && g_strv_equal ((const char *const *) priv->query_parts, + (const char *const *) parts)) { + phosh_dbus_search_complete_query (interface, invocation, FALSE); + phosh_dbus_search_emit_query_finished (interface); + + return TRUE; + } + + g_cancellable_cancel (priv->cancellable); + /* Avoid reusing cancellable by creating a new one */ + g_clear_object (&priv->cancellable); + priv->cancellable = g_cancellable_new (); + + if (len == 0) { + g_clear_pointer (&priv->query, g_free); + g_clear_pointer (&priv->query_parts, g_strfreev); + + priv->doing_subsearch = FALSE; + + phosh_dbus_search_complete_query (interface, invocation, FALSE); + phosh_dbus_search_emit_query_finished (interface); + + return TRUE; + } + + if (priv->query != NULL) + priv->doing_subsearch = g_str_has_prefix (query, priv->query); + else + priv->doing_subsearch = FALSE; + + g_clear_pointer (&priv->query_parts, g_strfreev); + priv->query_parts = g_strdupv (parts); + priv->query = g_strdup (query); + + if (priv->search_timeout == 0) + priv->search_timeout = g_timeout_add (150, search_timeout, self); + + phosh_dbus_search_complete_query (interface, invocation, TRUE); + + return TRUE; +} + + +/* Sort algorithm taken straight from remoteSearch.js, comments and all */ +static int +sort_sources (gconstpointer a, gconstpointer b, gpointer user_data) +{ + GAppInfo *app_a = NULL; + GAppInfo *app_b = NULL; + const char *app_id_a = NULL; + const char *app_id_b = NULL; + GStrv order = user_data; + int idx_a = -1; + int idx_b = -1; + int i = 0; + + g_return_val_if_fail (a != NULL, -1); + g_return_val_if_fail (b != NULL, -1); + + app_a = phosh_search_source_get_app_info ((PhoshSearchSource *) a); + app_b = phosh_search_source_get_app_info ((PhoshSearchSource *) b); + + g_return_val_if_fail (G_IS_APP_INFO (app_a), -1); + g_return_val_if_fail (G_IS_APP_INFO (app_b), -1); + + app_id_a = g_app_info_get_id (app_a); + app_id_b = g_app_info_get_id (app_b); + + while ((order[i])) { + if (idx_a == -1 && g_strcmp0 (order[i], app_id_a) == 0) + idx_a = i; + + if (idx_b == -1 && g_strcmp0 (order[i], app_id_b) == 0) + idx_b = i; + + if (idx_a != -1 && idx_b != -1) + break; + + i++; + } + + /* if no provider is found in the order, use alphabetical order */ + if ((idx_a == -1) && (idx_b == -1)) + return g_utf8_collate (g_app_info_get_name (app_a), g_app_info_get_name (app_b)); + + /* if providerA isn't found, it's sorted after providerB */ + if (idx_a == -1) + return 1; + + /* if providerB isn't found, it's sorted after providerA */ + if (idx_b == -1) + return -1; + + /* finally, if both providers are found, return their order in the list */ + return (idx_a - idx_b); +} + + +static void +reload_providers (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + const char *const *data_dirs = g_get_system_data_dirs (); + const char *data_dir = NULL; + g_autolist (PhoshSearchSource) sources = NULL; + /* This skip the normal sorting */ + g_autoptr (PhoshSearchSource) settings = NULL; + g_auto (GStrv) enabled = NULL; + g_auto (GStrv) disabled = NULL; + g_auto (GStrv) sort_order = NULL; + GList *list; + int i = 0; + + if (g_settings_get_boolean (priv->settings, "disable-external")) + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + + enabled = g_settings_get_strv (priv->settings, "enabled"); + disabled = g_settings_get_strv (priv->settings, "disabled"); + sort_order = g_settings_get_strv (priv->settings, "sort-order"); + + while ((data_dir = data_dirs[i])) { + g_autofree char *dir = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (GDir) contents = NULL; + const char* name = NULL; + + i++; + + dir = g_build_filename (data_dir, "gnome-shell", "search-providers", NULL); + contents = g_dir_open (dir, 0, &error); + + if (error) { + if (!g_error_matches (error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + g_warning ("Can't look for in %s: %s", dir, error->message); + + g_clear_error (&error); + continue; + } + + while ((name = g_dir_read_name (contents))) { + g_autofree char *provider = NULL; + g_autofree char *bus_path = NULL; + g_autofree char *bus_name = NULL; + g_autofree char *desktop_id = NULL; + g_autoptr (GKeyFile) data = NULL; + g_autoptr (PhoshSearchProvider) provider_object = NULL; + g_autoptr (PhoshSearchSource) source = NULL; + g_autoptr (GAppInfo) info = NULL; + int version = 0; + gboolean autostart = TRUE; + gboolean autostart_tmp = FALSE; + gboolean default_disabled = FALSE; + gboolean default_disabled_tmp = FALSE; + + provider = g_build_filename (dir, name, NULL); + data = g_key_file_new (); + + g_key_file_load_from_file (data, provider, G_KEY_FILE_NONE, &error); + + if (error) { + g_warning ("Can't read %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + + if (!g_key_file_has_group (data, GROUP_NAME)) { + g_warning ("%s doesn't define a search provider", provider); + continue; + } + + + version = g_key_file_get_integer (data, GROUP_NAME, "Version", &error); + + if (error) { + g_warning ("Failed to fetch provider version %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + + if (version < 2) { + g_warning ("Provider %s implements version %i but we only support version 2 and up", provider, version); + continue; + } + + + desktop_id = g_key_file_get_string (data, GROUP_NAME, "DesktopId", &error); + if (error) { + g_warning ("Failed to fetch provider desktop id %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!desktop_id) { + g_warning ("Provider %s doesn't specify a desktop id", provider); + continue; + } + + + bus_name = g_key_file_get_string (data, GROUP_NAME, "BusName", &error); + if (error) { + g_warning ("Failed to fetch provider bus name %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!bus_name) { + g_warning ("Provider %s doesn't specify a bus name", provider); + continue; + } + + + bus_path = g_key_file_get_string (data, GROUP_NAME, "ObjectPath", &error); + if (error) { + g_warning ("Failed to fetch provider bus path %s: %s", provider, error->message); + g_clear_error (&error); + continue; + } + if (!bus_path) { + g_warning ("Provider %s doesn't specify a bus path", provider); + continue; + } + + if (g_hash_table_contains (priv->providers, bus_path)) { + g_debug ("We already have a provider for %s, ignoring %s", bus_path, provider); + continue; + } + + autostart_tmp = g_key_file_get_boolean (data, GROUP_NAME, "AutoStart", &error); + + if (G_LIKELY (error)) + g_clear_error (&error); + else + autostart = autostart_tmp; + + default_disabled_tmp = g_key_file_get_boolean (data, GROUP_NAME, "DefaultDisabled", &error); + if (G_LIKELY (error)) + g_clear_error (&error); + else + default_disabled = default_disabled_tmp; + + if (!default_disabled) { + if (g_strv_contains ((const char * const*) disabled, desktop_id)) { + g_debug ("Provider %s has been disabled", provider); + continue; + } + } else { + if (!g_strv_contains ((const char * const*) enabled, desktop_id)) { + g_debug ("Provider %s hasn't been enabled", provider); + continue; + } + } + + provider_object = phosh_search_provider_new (desktop_id, + priv->cancellable, + bus_path, + bus_name, + autostart, + default_disabled); + + if (G_UNLIKELY (g_str_equal (desktop_id, "org.gnome.Settings.desktop"))) { + info = G_APP_INFO (g_desktop_app_info_new (desktop_id)); + settings = phosh_search_source_new (bus_path, info); + } else { + info = G_APP_INFO (g_desktop_app_info_new (desktop_id)); + source = phosh_search_source_new (bus_path, info); + sources = g_list_prepend (sources, phosh_search_source_ref (source)); + } + + g_hash_table_insert (priv->providers, g_strdup (bus_path), g_object_ref (provider_object)); + } + } + + sources = g_list_sort_with_data (sources, sort_sources, sort_order); + + if (settings) + sources = g_list_prepend (sources, phosh_search_source_ref (settings)); + + list = sources; + i = 0; + + while (list) { + phosh_search_source_set_position (list->data, i); + + i++; + list = g_list_next (list); + } + + g_list_free_full (priv->sources, (GDestroyNotify) phosh_search_source_unref); + priv->sources = g_list_copy_deep (sources, (GCopyFunc) phosh_search_source_ref, NULL); +} + + +static void +reload_providers_apps_changed (GAppInfoMonitor *monitor, PhoshSearchApplication *self) +{ + reload_providers (self); +} + + +static void +phosh_search_application_init (PhoshSearchApplication *self) +{ + PhoshSearchApplicationPrivate *priv = phosh_search_application_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->doing_subsearch = FALSE; + priv->search_timeout = 0; + priv->outstanding_searches = 0; + + priv->splitter = g_regex_new ("\\s+", G_REGEX_CASELESS | G_REGEX_MULTILINE, 0, &error); + if (error) + g_error ("Bad Regex: %s", error->message); + + priv->last_results = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_variant_unref); + priv->providers = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) g_object_unref); + + priv->cancellable = g_cancellable_new (); + + priv->settings = g_settings_new (SEARCH_PROVIDERS_SCHEMA); + g_object_connect (priv->settings, + "swapped-object-signal::changed::disabled", reload_providers, self, + "swapped-object-signal::changed::enabled", reload_providers, self, + "swapped-object-signal::changed::disable-external", reload_providers, self, + "swapped-object-signal::changed::sort-order", reload_providers, self, + NULL); + + g_signal_connect (g_app_info_monitor_get (), + "changed", + G_CALLBACK (reload_providers_apps_changed), self); + + priv->object = phosh_dbus_search_skeleton_new (); + g_object_connect (priv->object, + "object-signal::handle-launch-source", launch_source, self, + "object-signal::handle-activate-result", activate_result, self, + "object-signal::handle-get-sources", get_sources, self, + "object-signal::handle-query", query, self, + "object-signal::handle-get-last-results", get_last_results, self, + NULL); + + reload_providers (self); + + g_application_hold (G_APPLICATION (self)); +} + + +static gboolean +on_shutdown_signal (gpointer user_data) +{ + GApplication *app = G_APPLICATION (user_data); + + g_debug ("Exiting gracefully."); + + g_application_release (app); + + return G_SOURCE_REMOVE; +} + + +int +main (int argc, char **argv) +{ + g_autoptr (GApplication) app = NULL; + + app = g_object_new (PHOSH_TYPE_SEARCH_APPLICATION, + "application-id", "mobi.phosh.Shell.Search", + "flags", G_APPLICATION_IS_SERVICE, + NULL); + g_unix_signal_add (SIGTERM, on_shutdown_signal, app); + g_unix_signal_add (SIGINT, on_shutdown_signal, app); + + return g_application_run (app, argc, argv); +} diff --git a/searchd/searchd.h b/searchd/searchd.h new file mode 100644 index 000000000..dd56c79cd --- /dev/null +++ b/searchd/searchd.h @@ -0,0 +1,21 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_APPLICATION phosh_search_application_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchApplication, phosh_search_application, PHOSH, SEARCH_APPLICATION, GApplication) + +struct _PhoshSearchApplicationClass +{ + GApplicationClass parent_class; +}; + +G_END_DECLS diff --git a/src/activity.c b/src/activity.c new file mode 100644 index 000000000..7cc4055b9 --- /dev/null +++ b/src/activity.c @@ -0,0 +1,880 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2024-2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-activity" + +#include "phosh-config.h" +#include "activity.h" +#include "shell-priv.h" +#include "swipe-away-bin.h" +#include "thumbnail.h" +#include "util.h" +#include "app-grid-button.h" + +/** + * PhoshActivity: + * + * An app in the favorites overview + * + * The #PhoshActivity is used to select a running application in the overview. + */ + +/* Icons actually sized according to the pixel-size set in the template */ +#define ACTIVITY_ICON_SIZE -1 + +enum { + CLICKED, + CLOSED, + FULLSCREENED, + RESIZED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +enum { + PROP_0, + PROP_APP_INFO, + PROP_APP_ID, + PROP_PARENT_APP_ID, + PROP_MAXIMIZED, + PROP_FULLSCREEN, + PROP_WIN_WIDTH, + PROP_WIN_HEIGHT, + PROP_HAS_THUMBNAIL, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +typedef struct { + GtkWidget *swipe_bin; + GtkWidget *icon; + GtkWidget *box; + GtkWidget *revealer_close; + GtkWidget *revealer_unfullscreen; + GtkWidget *btn_close; + GtkWidget *btn_unfullscreen; + GtkWidget *preview; + GtkWidget *button; + GtkWidget *spinner; + + gboolean maximized; + gboolean fullscreen; + int win_width; + int win_height; + + char *app_id; + char *parent_app_id; + + cairo_surface_t *surface; + PhoshThumbnail *thumbnail; + + gboolean hovering; + guint remove_timeout_id; + GtkAllocation allocation; + + GAppInfo *app_info; +} PhoshActivityPrivate; + + +struct _PhoshActivity { + GtkEventBox parent; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshActivity, phosh_activity, GTK_TYPE_EVENT_BOX) + + +static void +set_fullscreen (PhoshActivity *self, gboolean fullscreen) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + priv->fullscreen = fullscreen; + phosh_util_toggle_style_class (GTK_WIDGET (self), "phosh-fullscreen", priv->fullscreen); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer_unfullscreen), priv->fullscreen); +} + + +static void +set_win_width (PhoshActivity *self, int width) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + if (width == priv->win_width) + return; + + priv->win_width = width; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WIN_WIDTH]); +} + + +static void +set_win_height (PhoshActivity *self, int height) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + if (height == priv->win_height) + return; + + priv->win_height = height; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_WIN_HEIGHT]); +} + + +static void +phosh_activity_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshActivity *self = PHOSH_ACTIVITY (object); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + g_set_object (&priv->app_info, g_value_get_object (value)); + break; + case PROP_APP_ID: + g_free (priv->app_id); + priv->app_id = g_value_dup_string (value); + break; + case PROP_PARENT_APP_ID: + g_free (priv->parent_app_id); + priv->parent_app_id = g_value_dup_string (value); + break; + case PROP_MAXIMIZED: + priv->maximized = g_value_get_boolean (value); + phosh_util_toggle_style_class (GTK_WIDGET (self), "phosh-maximized", priv->maximized); + break; + case PROP_FULLSCREEN: + set_fullscreen (self, g_value_get_boolean (value)); + break; + case PROP_WIN_WIDTH: + set_win_width (self, g_value_get_int (value)); + break; + case PROP_WIN_HEIGHT: + set_win_height (self, g_value_get_int (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_activity_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshActivity *self = PHOSH_ACTIVITY (object); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + switch (property_id) { + case PROP_APP_INFO: + g_value_set_object (value, phosh_activity_get_app_info (self)); + break; + case PROP_APP_ID: + g_value_set_string (value, phosh_activity_get_app_id (self)); + break; + case PROP_PARENT_APP_ID: + g_value_set_string (value, priv->parent_app_id); + break; + case PROP_MAXIMIZED: + g_value_set_boolean (value, priv->maximized); + break; + case PROP_FULLSCREEN: + g_value_set_boolean (value, priv->fullscreen); + break; + case PROP_WIN_WIDTH: + g_value_set_int (value, priv->win_width); + break; + case PROP_WIN_HEIGHT: + g_value_set_int (value, priv->win_height); + break; + case PROP_HAS_THUMBNAIL: + g_value_set_boolean (value, phosh_activity_get_has_thumbnail (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +clicked_cb (PhoshActivity *self) +{ + g_signal_emit (self, signals[CLICKED], 0); +} + + +static void +closed_cb (PhoshActivity *self) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + phosh_swipe_away_bin_remove (PHOSH_SWIPE_AWAY_BIN (priv->swipe_bin)); +} + + +static void +on_unfullscreen_clicked (PhoshActivity *self) +{ + g_signal_emit (self, signals[FULLSCREENED], 0, FALSE); + g_signal_emit (self, signals[CLICKED], 0); +} + + +static void +on_remove_timeout (gpointer data) +{ + PhoshActivity *self = PHOSH_ACTIVITY (data); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + phosh_swipe_away_bin_undo (PHOSH_SWIPE_AWAY_BIN (priv->swipe_bin)); + + priv->remove_timeout_id = 0; +} + + +static void +removed_cb (PhoshActivity *self) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + if (priv->remove_timeout_id) + g_source_remove (priv->remove_timeout_id); + + priv->remove_timeout_id = g_timeout_add_seconds_once (1, on_remove_timeout, self); + g_source_set_name_by_id (priv->remove_timeout_id, "[phosh] remove_timeout_id"); + + g_signal_emit (self, signals[CLOSED], 0); +} + + +static float +get_scale (PhoshActivity *self) +{ + float scale; + int width, height, image_width, image_height; + PhoshActivityPrivate *priv; + + priv = phosh_activity_get_instance_private (self); + width = gtk_widget_get_allocated_width (priv->preview); + height = gtk_widget_get_allocated_height (priv->preview); + + if (!priv->surface) + return 1.0; + + image_width = cairo_image_surface_get_width (priv->surface); + image_height = cairo_image_surface_get_height (priv->surface); + + scale = MIN (width / (float)image_width, height / (float)image_height); + + return scale; +} + + +static void +draw_rounded_rect (cairo_t *ctx, double x, double y, double width, double height, double radius) +{ + cairo_new_sub_path (ctx); + cairo_arc (ctx, x + width - radius, y + radius, radius, -0.5 * M_PI, 0); + cairo_arc (ctx, x + width - radius, y + height - radius, radius, 0, 0.5 * M_PI); + cairo_arc (ctx, x + radius, y + height - radius, radius, 0.5 * M_PI, M_PI); + cairo_arc (ctx, x + radius, y + radius, radius, M_PI, 1.5 * M_PI); + cairo_close_path (ctx); +} + + +static gboolean +draw_cb (PhoshActivity *self, cairo_t *cairo, GtkDrawingArea *area) +{ + int width, height, image_width, image_height, border_radius, x, y = 0; + float scale; + PhoshActivityPrivate *priv; + GtkStyleContext *context; + + g_return_val_if_fail (PHOSH_IS_ACTIVITY (self), FALSE); + g_return_val_if_fail (GTK_IS_DRAWING_AREA (area), FALSE); + + priv = phosh_activity_get_instance_private (self); + + context = gtk_widget_get_style_context (GTK_WIDGET (area)); + width = gtk_widget_get_allocated_width (GTK_WIDGET (area)); + height = gtk_widget_get_allocated_height (GTK_WIDGET (area)); + + gtk_render_background (context, cairo, 0, 0, width, height); + + if (!priv->surface) + return FALSE; + + image_width = cairo_image_surface_get_width (priv->surface); + image_height = cairo_image_surface_get_height (priv->surface); + + scale = get_scale (self); + cairo_scale (cairo, scale, scale); + + x = (width - image_width * scale) / 2.0 / scale; + + gtk_style_context_get (context, + gtk_style_context_get_state (context), + GTK_STYLE_PROPERTY_BORDER_RADIUS, &border_radius, + NULL); + draw_rounded_rect (cairo, x, y, image_width, image_height, border_radius); + cairo_set_source_surface (cairo, priv->surface, x, y); + cairo_fill (cairo); + + return FALSE; +} + + +static void +size_allocate_cb (PhoshActivity *self, GtkAllocation *alloc, GtkDrawingArea *area) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + gboolean changed = alloc->width != priv->allocation.width || + alloc->height != priv->allocation.height; + + priv->allocation = *alloc; + + if (changed) + g_signal_emit (self, signals[RESIZED], 0, alloc); +} + + +static void +phosh_activity_constructed (GObject *object) +{ + PhoshActivity *self = PHOSH_ACTIVITY (object); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + GIcon *icon = NULL; + + if (priv->app_info == NULL) + priv->app_info = G_APP_INFO (phosh_get_desktop_app_info_for_app_id (priv->app_id)); + + if (priv->app_info) + icon = g_app_info_get_icon (priv->app_info); + + if (!icon && priv->parent_app_id) { + priv->app_info = G_APP_INFO (phosh_get_desktop_app_info_for_app_id (priv->parent_app_id)); + if (priv->app_info) + icon = g_app_info_get_icon (priv->app_info); + } + + if (icon) { + gtk_image_set_from_gicon (GTK_IMAGE (priv->icon), icon, ACTIVITY_ICON_SIZE); + } else { + gtk_image_set_from_icon_name (GTK_IMAGE (priv->icon), + PHOSH_APP_UNKNOWN_ICON, + ACTIVITY_ICON_SIZE); + } + + phosh_util_toggle_style_class (GTK_WIDGET (self), "phosh-empty", TRUE); + + G_OBJECT_CLASS (phosh_activity_parent_class)->constructed (object); +} + + +static void +phosh_activity_dispose (GObject *object) +{ + PhoshActivity *self = PHOSH_ACTIVITY (object); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + g_clear_pointer (&priv->surface, cairo_surface_destroy); + g_clear_object (&priv->thumbnail); + g_clear_object (&priv->app_info); + + if (priv->remove_timeout_id) { + g_source_remove (priv->remove_timeout_id); + priv->remove_timeout_id = 0; + } + + G_OBJECT_CLASS (phosh_activity_parent_class)->dispose (object); +} + + +static void +phosh_activity_finalize (GObject *object) +{ + PhoshActivity *self = PHOSH_ACTIVITY (object); + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + g_free (priv->parent_app_id); + g_free (priv->app_id); + + G_OBJECT_CLASS (phosh_activity_parent_class)->finalize (object); +} + + +static GtkSizeRequestMode +phosh_activity_get_request_mode (GtkWidget *widget) +{ + return GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; +} + + +static void +phosh_activity_get_preferred_height (GtkWidget *widget, + int *min, + int *nat) +{ + PhoshActivityPrivate *priv; + int smallest = 0; + int box_smallest = 0; + int parent_nat; + + g_return_if_fail (PHOSH_IS_ACTIVITY (widget)); + priv = phosh_activity_get_instance_private (PHOSH_ACTIVITY (widget)); + + GTK_WIDGET_CLASS (phosh_activity_parent_class)->get_preferred_height (widget, + &smallest, + &parent_nat); + gtk_widget_get_preferred_width (priv->box, &box_smallest, NULL); + + smallest = MAX (smallest, box_smallest); + + if (min) + *min = smallest; + + if (nat) + *nat = smallest; +} + + +static void +phosh_activity_get_preferred_width_for_height (GtkWidget *widget, + int height, + int *min, + int *nat) +{ + PhoshActivityPrivate *priv; + int smallest = 0; + int box_smallest = 0; + int size; + int parent_nat; + int margin_start, margin_end, margin_top, margin_bottom; + double aspect_ratio; + + g_return_if_fail (PHOSH_IS_ACTIVITY (widget)); + priv = phosh_activity_get_instance_private (PHOSH_ACTIVITY (widget)); + + GTK_WIDGET_CLASS (phosh_activity_parent_class)->get_preferred_width_for_height (widget, + height, + &smallest, + &parent_nat); + gtk_widget_get_preferred_width_for_height (priv->box, height, &box_smallest, NULL); + + smallest = MAX (smallest, box_smallest); + + margin_start = gtk_widget_get_margin_start (priv->preview); + margin_end = gtk_widget_get_margin_end (priv->preview); + margin_top = gtk_widget_get_margin_top (priv->preview); + margin_bottom = gtk_widget_get_margin_bottom (priv->preview); + + aspect_ratio = (double) priv->win_width / priv->win_height; + size = MAX (smallest, + (height - margin_top - margin_bottom) * aspect_ratio) + margin_start + margin_end; + + if (min) + *min = size; + + if (nat) + *nat = size; +} + + +static void +phosh_activity_get_preferred_height_for_width (GtkWidget *widget, + int width, + int *min, + int *nat) +{ + phosh_activity_get_preferred_height (widget, min, nat); +} + + +static void +set_hovering (PhoshActivity *self, + gboolean hovering) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + if (!phosh_activity_get_has_thumbnail (self)) + return; + + if (hovering == priv->hovering) + return; + + priv->hovering = hovering; + + /* Revealer won't animate if not mapped, show it preemptively */ + if (hovering) + gtk_widget_set_visible (priv->revealer_close, TRUE); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->revealer_close), hovering); +} + + +static gboolean +phosh_activity_enter_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + if (event->window != gtk_widget_get_window (widget) || + event->detail == GDK_NOTIFY_INFERIOR) + return GDK_EVENT_PROPAGATE; + + /* enter-notify never happens on touch, so we don't need to check it */ + set_hovering (PHOSH_ACTIVITY (widget), TRUE); + + return GDK_EVENT_PROPAGATE; +} + + +static gboolean +phosh_activity_leave_notify_event (GtkWidget *widget, + GdkEventCrossing *event) +{ + if (event->window != gtk_widget_get_window (widget) || + event->detail == GDK_NOTIFY_INFERIOR) + return GDK_EVENT_PROPAGATE; + + set_hovering (PHOSH_ACTIVITY (widget), FALSE); + + return GDK_EVENT_PROPAGATE; +} + + +static gboolean +phosh_activity_motion_notify_event (GtkWidget *widget, + GdkEventMotion *event) +{ + GdkDevice *source_device = gdk_event_get_source_device ((GdkEvent *) event); + GdkInputSource input_source = gdk_device_get_source (source_device); + + if (input_source != GDK_SOURCE_TOUCHSCREEN) + set_hovering (PHOSH_ACTIVITY (widget), TRUE); + + return GDK_EVENT_PROPAGATE; +} + + +static gboolean +phosh_activity_key_press_event (GtkWidget *self, GdkEventKey *event) +{ + gboolean handled = FALSE; + PhoshActivityPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_ACTIVITY (self), FALSE); + priv = phosh_activity_get_instance_private (PHOSH_ACTIVITY (self)); + + switch (event->keyval) { + case GDK_KEY_Return: + gtk_button_clicked (GTK_BUTTON (priv->button)); + handled = TRUE; + break; + default: + /* nothing to do */ + break; + } + + return handled; +} + + +static void +phosh_activity_unmap (GtkWidget *widget) +{ + set_hovering (PHOSH_ACTIVITY (widget), FALSE); + + GTK_WIDGET_CLASS (phosh_activity_parent_class)->unmap (widget); +} + + +static void +phosh_activity_class_init (PhoshActivityClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_activity_constructed; + object_class->dispose = phosh_activity_dispose; + object_class->finalize = phosh_activity_finalize; + + object_class->set_property = phosh_activity_set_property; + object_class->get_property = phosh_activity_get_property; + + widget_class->get_request_mode = phosh_activity_get_request_mode; + widget_class->get_preferred_height = phosh_activity_get_preferred_height; + widget_class->get_preferred_height_for_width = phosh_activity_get_preferred_height_for_width; + widget_class->get_preferred_width_for_height = phosh_activity_get_preferred_width_for_height; + widget_class->enter_notify_event = phosh_activity_enter_notify_event; + widget_class->leave_notify_event = phosh_activity_leave_notify_event; + widget_class->motion_notify_event = phosh_activity_motion_notify_event; + widget_class->key_press_event = phosh_activity_key_press_event; + widget_class->unmap = phosh_activity_unmap; + + /** + * PhoshActivity:app-info: + * + * The app-info of the activity + */ + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", "", "", + G_TYPE_APP_INFO, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshActivity:app-id: + * + * The app-id of the activity + */ + props[PROP_APP_ID] = + g_param_spec_string ("app-id", "", "", + "", + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshActivity:parent-app-id: + * + * The app-id of the parent activity (if any) + */ + props[PROP_PARENT_APP_ID] = + g_param_spec_string ("parent-app-id", "", "", + NULL, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshActivity:maximized: + * + * Whether the window is maximized + */ + props[PROP_MAXIMIZED] = + g_param_spec_boolean ("maximized", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshActivity:fullscreen: + * + * Whether the window is presented fullscreen + */ + props[PROP_FULLSCREEN] = + g_param_spec_boolean ("fullscreen", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshActivity:win-width: + * + * The window's width + */ + props[PROP_WIN_WIDTH] = + g_param_spec_int ("win-width", "", "", + 0, G_MAXINT, 300, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshActivity:win-height: + * + * The window's height + */ + props[PROP_WIN_HEIGHT] = + g_param_spec_int ("win-height", "", "", + 0, G_MAXINT, 300, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshActivity:has-thumbnail: + * + * Whether the activity is backed by a thumbnail + */ + props[PROP_HAS_THUMBNAIL] = + g_param_spec_boolean ("has-thumbnail", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[CLICKED] = g_signal_new ("clicked", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); + /** + * PhoshActivity::fullscreened + * @self: The activity + * @fullscreen: Whether the activity should be fullscreened or + * unfullscreened + * + * The fullscreen state of the activity should be changed. + */ + signals[FULLSCREENED] = g_signal_new ("fullscreened", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, 1, + G_TYPE_BOOLEAN); + + signals[RESIZED] = g_signal_new ("resized", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + GDK_TYPE_RECTANGLE | G_SIGNAL_TYPE_STATIC_SCOPE); + + g_type_ensure (PHOSH_TYPE_SWIPE_AWAY_BIN); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/activity.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, btn_close); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, btn_unfullscreen); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, button); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, preview); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, swipe_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, icon); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, box); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, revealer_close); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, revealer_unfullscreen); + gtk_widget_class_bind_template_child_private (widget_class, PhoshActivity, spinner); + gtk_widget_class_bind_template_callback (widget_class, clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, closed_cb); + gtk_widget_class_bind_template_callback (widget_class, draw_cb); + gtk_widget_class_bind_template_callback (widget_class, on_unfullscreen_clicked); + gtk_widget_class_bind_template_callback (widget_class, removed_cb); + gtk_widget_class_bind_template_callback (widget_class, size_allocate_cb); + + gtk_widget_class_set_css_name (widget_class, "phosh-activity"); +} + + +static void +phosh_activity_init (PhoshActivity *self) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->win_height = 300; + priv->win_width = 300; +} + + +GtkWidget * +phosh_activity_new (const char *app_id) +{ + return g_object_new (PHOSH_TYPE_ACTIVITY, + "app-id", app_id, + NULL); +} + + +const char * +phosh_activity_get_app_id (PhoshActivity *self) +{ + PhoshActivityPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_ACTIVITY (self), NULL); + priv = phosh_activity_get_instance_private (self); + + if (priv->app_id) + return priv->app_id; + + if (priv->app_info) + return g_app_info_get_id (priv->app_info); + + return NULL; +} + +/** + * phosh_activity_set_thumbnail: + * @self: the activity + * @thumbnail:(transfer full): the thumbnail + * + * Sets the given thumbnail + */ +void +phosh_activity_set_thumbnail (PhoshActivity *self, PhoshThumbnail *thumbnail) +{ + PhoshActivityPrivate *priv; + gpointer data; + guint w, width, height, stride, margin; + float scale; + gboolean has_thumbnail; + + g_return_if_fail (PHOSH_IS_ACTIVITY (self)); + priv = phosh_activity_get_instance_private (self); + + has_thumbnail = !!priv->thumbnail; + g_clear_object (&priv->thumbnail); + priv->thumbnail = thumbnail; + + data = phosh_thumbnail_get_image (thumbnail); + phosh_thumbnail_get_size (thumbnail, &width, &height, &stride); + + g_clear_pointer (&priv->surface, cairo_surface_destroy); + priv->surface = cairo_image_surface_create_for_data (data, + CAIRO_FORMAT_ARGB32, + width, height, stride); + + phosh_util_toggle_style_class (GTK_WIDGET (self), "phosh-empty", FALSE); + + /* Make sure buttons are over the thumbnail */ + w = gtk_widget_get_allocated_width (GTK_WIDGET (self)); + scale = get_scale (self); + margin = w ? (w - (width * scale)) / 2 : 0; + gtk_widget_set_margin_start (priv->btn_unfullscreen, margin); + gtk_widget_set_margin_end (priv->btn_close, margin); + + gtk_widget_queue_draw (GTK_WIDGET (self)); + + if (has_thumbnail != !!priv->thumbnail) + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_THUMBNAIL]); +} + +void +phosh_activity_get_thumbnail_allocation (PhoshActivity *self, GtkAllocation *allocation) +{ + PhoshActivityPrivate *priv; + + g_return_if_fail (allocation); + g_return_if_fail (PHOSH_IS_ACTIVITY (self)); + priv = phosh_activity_get_instance_private (self); + + *allocation = priv->allocation; +} + + +gboolean +phosh_activity_get_has_thumbnail (PhoshActivity *self) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_ACTIVITY (self), FALSE); + + return !!priv->thumbnail; +} + + +GAppInfo * +phosh_activity_get_app_info (PhoshActivity *self) +{ + PhoshActivityPrivate *priv = phosh_activity_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_ACTIVITY (self), NULL); + + return priv->app_info; +} diff --git a/src/activity.h b/src/activity.h new file mode 100644 index 000000000..bfc6921d6 --- /dev/null +++ b/src/activity.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "thumbnail.h" + +#include + +#include + +#define PHOSH_TYPE_ACTIVITY (phosh_activity_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshActivity, phosh_activity, PHOSH, ACTIVITY, GtkEventBox) + +GtkWidget *phosh_activity_new (const char *app_id); +const char *phosh_activity_get_app_id (PhoshActivity *self); +void phosh_activity_set_thumbnail (PhoshActivity *self, + PhoshThumbnail *thumbnail); +void phosh_activity_get_thumbnail_allocation (PhoshActivity *self, + GtkAllocation *allocation); +gboolean phosh_activity_get_has_thumbnail (PhoshActivity *self); +GAppInfo * phosh_activity_get_app_info (PhoshActivity *self); diff --git a/src/ambient.c b/src/ambient.c new file mode 100644 index 000000000..6fc839275 --- /dev/null +++ b/src/ambient.c @@ -0,0 +1,606 @@ +/* + * Copyright (C) 2022 Purism SPC + * 2023-2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-ambient" + +#include "phosh-config.h" +#include "animation.h" +#include "fader.h" +#include "ambient.h" +#include "shell-priv.h" +#include "sensor-proxy-manager.h" +#include "util.h" + +#define INTERFACE_SCHEMA "org.gnome.desktop.interface" +#define HIGH_CONTRAST_THEME "HighContrast" +#define KEY_GTK_THEME "gtk-theme" +#define KEY_ICON_THEME "icon-theme" + +#define PHOSH_SCHEMA "sm.puri.phosh" +#define KEY_AUTOMATIC_HC "automatic-high-contrast" +#define KEY_AUTOMATIC_HC_THRESHOLD "automatic-high-contrast-threshold" + +#define POWER_SCHEMA "org.gnome.settings-daemon.plugins.power" +#define KEY_AMBIENT_ENABLED "ambient-enabled" + +#define NUM_VALUES 3 + +/** + * PhoshAmbient: + * + * Ambient light sensor handling + * + * #PhoshAmbient handles enabling and disabling the ambient detection + * based and toggles related actions. + */ + +enum { + PROP_0, + PROP_SENSOR_PROXY_MANAGER, + PROP_AUTO_BRIGHTNESS_ENABLED, + PROP_LIGHT_LEVEL, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + + +typedef struct _PhoshAmbient { + GObject parent; + + int claimed; + PhoshSensorProxyManager *sensor_proxy_manager; + GCancellable *cancel; + + GSettings *phosh_settings; + GSettings *interface_settings; + GSettings *power_settings; + gboolean auto_hc; + gboolean use_hc; + gboolean auto_brightness; + double light_level; + gboolean blanked; + + guint sample_id; + GArray *values; + + PhoshFader *fader; + guint fader_id; +} PhoshAmbient; + +G_DEFINE_TYPE (PhoshAmbient, phosh_ambient, G_TYPE_OBJECT); + + +static void +phosh_ambient_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAmbient *self = PHOSH_AMBIENT (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + /* construct only */ + self->sensor_proxy_manager = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_ambient_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAmbient *self = PHOSH_AMBIENT (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + g_value_set_object (value, self->sensor_proxy_manager); + break; + case PROP_AUTO_BRIGHTNESS_ENABLED: + g_value_set_boolean (value, self->auto_brightness); + break; + case PROP_LIGHT_LEVEL: + g_value_set_double (value, self->light_level); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static gboolean +on_fade_in_done (PhoshAmbient *self) +{ + if (self->use_hc) { + g_settings_set_string (self->interface_settings, KEY_GTK_THEME, HIGH_CONTRAST_THEME); + } else { + g_settings_reset (self->interface_settings, KEY_GTK_THEME); + g_settings_reset (self->interface_settings, KEY_ICON_THEME); + } + + phosh_fader_hide (self->fader); + return G_SOURCE_REMOVE; +} + + +static void +switch_theme (PhoshAmbient *self, gboolean use_hc) +{ + const char *style_class; + + if (use_hc == self->use_hc) + return; + + style_class = use_hc ? "phosh-fader-theme-to-hc" : "phosh-fader-theme-from-hc"; + self->fader = g_object_new (PHOSH_TYPE_FADER, + "style-class", style_class, + "fade-out-time", 1500, + "fade-out-type", PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC, + NULL); + gtk_widget_set_visible (GTK_WIDGET (self->fader), TRUE); + + self->fader_id = g_timeout_add (100 * PHOSH_ANIMATION_SLOWDOWN, + G_SOURCE_FUNC (on_fade_in_done), + self); + g_source_set_name_by_id (self->fader_id, "[phosh] ambient fader"); + + self->use_hc = use_hc; +} + + +static gboolean +on_ambient_sample_for_hc (gpointer data) +{ + PhoshAmbient *self = PHOSH_AMBIENT (data); + double level, threshold; + double avg = 0.0; + gboolean use_hc; + + level = phosh_dbus_sensor_proxy_get_light_level (PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + g_array_append_val (self->values, level); + + if (self->values->len < NUM_VALUES) + return G_SOURCE_CONTINUE; + + for (int i = 0; i < self->values->len; i++) + avg += g_array_index (self->values, double, i); + + avg /= self->values->len; + threshold = g_settings_get_uint (self->phosh_settings, KEY_AUTOMATIC_HC_THRESHOLD); + use_hc = avg > threshold; + + g_debug ("Avg: %f Switching theme to hc: %d", avg, use_hc); + switch_theme (self, use_hc); + + g_array_set_size (self->values, 0); + self->sample_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +stop_high_contrast_sampling (PhoshAmbient *self) +{ + g_clear_handle_id (&self->sample_id, g_source_remove); + g_array_set_size (self->values, 0); +} + + +static void +check_high_contrast (PhoshAmbient *self, double level) +{ + double hyst, threshold; + gboolean wants_hc; + + if (!self->auto_hc) + return; + + /* Currently sampling, ignoring changes */ + if (self->sample_id) + return; + + threshold = g_settings_get_uint (self->phosh_settings, KEY_AUTOMATIC_HC_THRESHOLD); + /* Use a bit of hysteresis to not switch too often around the threshold */ + hyst = self->use_hc ? 0.9 : 1.1; + threshold *= hyst; + + wants_hc = level > threshold; + /* New value wouldn't change anything, nothing to do */ + if (wants_hc == self->use_hc) + return; + + /* new value would change hc mode, sample to see if it should stick */ + g_return_if_fail (self->sample_id == 0); + g_return_if_fail (self->values->len == 0); + g_array_append_val (self->values, level); + self->sample_id = g_timeout_add_seconds (1, on_ambient_sample_for_hc, self); + g_source_set_name_by_id (self->sample_id, "[phosh] ambient_sample_for_hc"); +} + + +static void +on_ambient_light_level_changed (PhoshAmbient *self, + GParamSpec *pspec, + PhoshSensorProxyManager *sensor) +{ + double level; + const char *unit; + PhoshDBusSensorProxy *proxy; + + if (!self->claimed) + return; + + proxy = PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager); + level = phosh_dbus_sensor_proxy_get_light_level (proxy); + unit = phosh_dbus_sensor_proxy_get_light_level_unit (proxy); + if (!unit || g_ascii_strcasecmp (unit, "lux") != 0) { + /* For vendor values we don't know if small or large values mean bright or dark so be conservative */ + g_warning_once ("Unknown light level unit %s", unit); + return; + } + + g_debug ("Ambient light changed: %.2f %s", level, unit); + if (!G_APPROX_VALUE (self->light_level, level, FLT_EPSILON)) { + self->light_level = level; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LIGHT_LEVEL]); + } + + check_high_contrast (self, level); +} + + +static void +update_auto_brightness_enabled (PhoshAmbient *self) +{ + gboolean auto_brightness; + + g_return_if_fail (self->claimed >= 0); + + auto_brightness = (self->claimed && + g_settings_get_boolean (self->power_settings, KEY_AMBIENT_ENABLED)); + + if (self->auto_brightness == auto_brightness) + return; + + self->auto_brightness = auto_brightness; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AUTO_BRIGHTNESS_ENABLED]); +} + + +static void +on_ambient_claimed (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSensorProxy *proxy = PHOSH_DBUS_SENSOR_PROXY (source_object); + PhoshAmbient *self = PHOSH_AMBIENT (user_data); + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (proxy)); + g_return_if_fail (proxy == PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_claim_light_finish (proxy, res, &err); + if (!success) { + g_warning ("Failed to claim ambient sensor: %s", err->message); + return; + } + + g_debug ("Claimed ambient sensor"); + self->claimed++; + + update_auto_brightness_enabled (self); + on_ambient_light_level_changed (self, NULL, self->sensor_proxy_manager); +} + + +static void +on_ambient_released (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSensorProxy *proxy = PHOSH_DBUS_SENSOR_PROXY (source_object); + PhoshAmbient *self = PHOSH_AMBIENT (user_data); + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (self->claimed > 0); + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (proxy)); + g_return_if_fail (proxy == PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_release_light_finish (proxy, res, &err); + if (success) { + g_debug ("Released ambient light sensor"); + } else { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + g_warning ("Failed to release ambient sensor: %s", err->message); + } + + self->claimed--; + update_auto_brightness_enabled (self); + stop_high_contrast_sampling (self); +} + + +static void +phosh_ambient_claim_light (PhoshAmbient *self, gboolean claim) +{ + PhoshDBusSensorProxy *proxy = PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager); + + if (claim == !!self->claimed) + return; + + g_debug ("Claiming sensor: %d", claim); + if (claim) { + phosh_dbus_sensor_proxy_call_claim_light (proxy, self->cancel, on_ambient_claimed, self); + } else { + phosh_dbus_sensor_proxy_call_release_light (proxy, self->cancel, on_ambient_released, self); + } +} + + +static void +maybe_claim (PhoshAmbient *self) +{ + gboolean auto_brightness, auto_hc, claim; + + g_return_if_fail (self->claimed >= 0); + + auto_brightness = g_settings_get_boolean (self->power_settings, KEY_AMBIENT_ENABLED); + auto_hc = g_settings_get_boolean (self->phosh_settings, KEY_AUTOMATIC_HC); + claim = auto_hc || auto_brightness; + + g_debug ("Auto brightness enabled: %d, Auto HC enabled: %d, claim: %d", + auto_brightness, auto_hc, claim); + + if (self->auto_hc == auto_hc && + self->auto_brightness == auto_brightness && + !!self->claimed == claim) { + return; + } + + self->auto_hc = auto_hc; + update_auto_brightness_enabled (self); + + if (claim) { + if (self->claimed) { + g_debug ("Already claimed, triggering update"); + on_ambient_light_level_changed (self, NULL, self->sensor_proxy_manager); + } else { + phosh_ambient_claim_light (self, TRUE); + } + } else { + phosh_ambient_claim_light (self, FALSE); + /* Switch back to normal theme */ + if (!self->auto_hc) + switch_theme (self, FALSE); + } +} + + +static void +on_settings_changed (PhoshAmbient *self) +{ + maybe_claim (self); +} + + +static void +on_has_ambient_light_changed (PhoshAmbient *self, + GParamSpec *pspec, + PhoshDBusSensorProxy *proxy) +{ + gboolean has_ambient; + + g_return_if_fail (self->claimed >= 0); + + has_ambient = phosh_dbus_sensor_proxy_get_has_ambient_light (proxy); + if (has_ambient) { + g_debug ("Ambient sensor appeared"); + maybe_claim (self); + return; + } + + if (!self->claimed) + return; + + g_debug ("Ambient sensor disappeared, marking unclaimed"); + self->claimed--; + update_auto_brightness_enabled (self); + stop_high_contrast_sampling (self); +} + + +static void +on_shell_state_changed (PhoshAmbient *self, GParamSpec *pspec, PhoshShell *shell) +{ + gboolean blanked; + PhoshShellStateFlags state; + + g_return_if_fail (PHOSH_IS_AMBIENT (self)); + g_return_if_fail (PHOSH_IS_SHELL (shell)); + + state = phosh_shell_get_state (shell); + blanked = !!(state & PHOSH_STATE_BLANKED); + + if (self->blanked == blanked) + return; + self->blanked = blanked; + + g_debug ("Shell blanked: %d", self->blanked); + /* Claim / unclaim the sensor on screen unblank / blank */ + if (blanked) + phosh_ambient_claim_light (self, FALSE); + else + maybe_claim (self); +} + + +static void +phosh_ambient_constructed (GObject *object) +{ + PhoshAmbient *self = PHOSH_AMBIENT (object); + + G_OBJECT_CLASS (phosh_ambient_parent_class)->constructed (object); + + g_object_connect (self->sensor_proxy_manager, + "swapped-signal::notify::light-level", + on_ambient_light_level_changed, + self, + "swapped-signal::notify::has-ambient-light", + on_has_ambient_light_changed, + self, + NULL); + + g_object_connect (self->phosh_settings, + "swapped-signal::changed::" KEY_AUTOMATIC_HC, + G_CALLBACK (on_settings_changed), + self, + "swapped-signal::changed::" KEY_AUTOMATIC_HC_THRESHOLD, + G_CALLBACK (on_settings_changed), + self, + NULL); + + g_signal_connect_object (phosh_shell_get_default (), + "notify::shell-state", + G_CALLBACK (on_shell_state_changed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_swapped (self->power_settings, + "changed::" KEY_AMBIENT_ENABLED, + G_CALLBACK (on_settings_changed), + self); + + on_has_ambient_light_changed (self, NULL, PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); +} + + +static void +phosh_ambient_dispose (GObject *object) +{ + PhoshAmbient *self = PHOSH_AMBIENT (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_handle_id (&self->sample_id, g_source_remove); + g_clear_pointer (&self->values, g_array_unref); + + if (self->sensor_proxy_manager) { + g_signal_handlers_disconnect_by_data (self->sensor_proxy_manager, self); + phosh_dbus_sensor_proxy_call_release_light_sync ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), NULL, NULL); + g_clear_object (&self->sensor_proxy_manager); + } + + g_clear_object (&self->phosh_settings); + g_clear_object (&self->interface_settings); + + g_clear_handle_id (&self->fader_id, g_source_remove); + g_clear_pointer (&self->fader, phosh_cp_widget_destroy); + + g_clear_object (&self->power_settings); + + G_OBJECT_CLASS (phosh_ambient_parent_class)->dispose (object); +} + + +static void +phosh_ambient_class_init (PhoshAmbientClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + + object_class->constructed = phosh_ambient_constructed; + object_class->dispose = phosh_ambient_dispose; + + object_class->set_property = phosh_ambient_set_property; + object_class->get_property = phosh_ambient_get_property; + + props[PROP_SENSOR_PROXY_MANAGER] = + g_param_spec_object ("sensor-proxy-manager", "", "", + PHOSH_TYPE_SENSOR_PROXY_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshAmbient:auto-brightness-enabled: + * + * If `TRUE` the display brightness should currently be adjusted to + * ambient light levels + */ + props[PROP_AUTO_BRIGHTNESS_ENABLED] = + g_param_spec_boolean ("auto-brightness-enabled", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshAmbient:light-level: + * + * The last light level reading of the ambient light sensor. + */ + props[PROP_LIGHT_LEVEL] = + g_param_spec_double ("light-level", "", "", + 0.0, G_MAXDOUBLE, 0.0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + + +static void +phosh_ambient_init (PhoshAmbient *self) +{ + g_autofree char *theme_name = NULL; + + /* Ensure initial sync */ + self->light_level = -1.0; + self->blanked = -1; + self->cancel = g_cancellable_new (); + + self->values = g_array_new (FALSE, FALSE, sizeof(double)); + + self->interface_settings = g_settings_new (INTERFACE_SCHEMA); + self->phosh_settings = g_settings_new (PHOSH_SCHEMA); + self->power_settings = g_settings_new (POWER_SCHEMA); + + /* Check whether we're already using the hc theme */ + theme_name = g_settings_get_string (self->interface_settings, KEY_GTK_THEME); + if (g_strcmp0 (theme_name, HIGH_CONTRAST_THEME) == 0) + self->use_hc = TRUE; +} + + +PhoshAmbient * +phosh_ambient_new (PhoshSensorProxyManager *sensor_proxy_manager) +{ + return g_object_new (PHOSH_TYPE_AMBIENT, + "sensor-proxy-manager", sensor_proxy_manager, + NULL); +} + + +gboolean +phosh_ambient_get_auto_brightness (PhoshAmbient *self) +{ + g_return_val_if_fail (PHOSH_IS_AMBIENT (self), FALSE); + + return self->auto_brightness; +} + + +double +phosh_ambient_get_light_level (PhoshAmbient *self) +{ + g_return_val_if_fail (PHOSH_IS_AMBIENT (self), FALSE); + + return self->light_level; +} diff --git a/src/ambient.h b/src/ambient.h new file mode 100644 index 000000000..8196ecd96 --- /dev/null +++ b/src/ambient.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "calls-manager.h" +#include "sensor-proxy-manager.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AMBIENT (phosh_ambient_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAmbient, phosh_ambient, PHOSH, AMBIENT, GObject); + +PhoshAmbient *phosh_ambient_new (PhoshSensorProxyManager *sensor_proxy_manager); +gboolean phosh_ambient_get_auto_brightness (PhoshAmbient *self); +double phosh_ambient_get_light_level (PhoshAmbient *self); + +G_END_DECLS diff --git a/src/animation.c b/src/animation.c new file mode 100644 index 000000000..560d67a76 --- /dev/null +++ b/src/animation.c @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ + +#include "phosh-config.h" + +#include "animation.h" +#include + +G_DEFINE_BOXED_TYPE (PhoshAnimation, phosh_animation, phosh_animation_ref, phosh_animation_unref) + +struct _PhoshAnimation +{ + gatomicrefcount ref_count; + + GtkWidget *widget; + + double value; + + double value_from; + double value_to; + gint64 duration; + PhoshAnimationType type; + + gint64 start_time; + guint tick_cb_id; + + PhoshAnimationValueCallback value_cb; + PhoshAnimationDoneCallback done_cb; + gpointer user_data; +}; + +static void +set_value (PhoshAnimation *self, + double value) +{ + self->value = value; + self->value_cb (value, self->user_data); +} + +#define LERP(a, b, t) (a) * (1.0 - (t)) + (b) * (t) + +/* Adapted from https://github.com/janrembold/es6-easings/blob/master/src/index.ts#L135 */ +/* TODO: Move to libhandy at some point */ +static double +ease_out_bounce (double t) +{ + double p; + + if (t < 1.0 / 2.75) + return 7.5625 * t * t; + + if (t < 2.0 / 2.75) { + p = t - (1.5 / 2.75); + + return 7.5625 * p * p + 0.75; + } + + if (t < 2.5 / 2.75) { + p = t - (2.25 / 2.75); + + return 7.5625 * p * p + 0.9375; + } + + p = t - (2.625 / 2.75); + + return 7.5625 * p * p + 0.984375; +} + + +static double +ease_in_quintic (double t) +{ + return t * t * t * t * t; +} + + +static double +ease_out_quintic (double t) +{ + double p = t - 1; + + return p * p * p * p * p + 1; +} + + +static inline double +interpolate (PhoshAnimationType type, double t) +{ + switch (type) { + case PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC: + return hdy_ease_out_cubic (t); + + case PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC: + return ease_in_quintic (t); + + case PHOSH_ANIMATION_TYPE_EASE_OUT_QUINTIC: + return ease_out_quintic (t); + + case PHOSH_ANIMATION_TYPE_EASE_OUT_BOUNCE: + return ease_out_bounce (t); + + default: + g_assert_not_reached (); + } +} + +static gboolean +tick_cb (GtkWidget *widget, + GdkFrameClock *frame_clock, + PhoshAnimation *self) +{ + gint64 frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000; + double t = (double) (frame_time - self->start_time) / self->duration; + + if (t >= 1) { + self->tick_cb_id = 0; + + set_value (self, self->value_to); + + g_signal_handlers_disconnect_by_func (self->widget, phosh_animation_stop, self); + + self->done_cb (self->user_data); + + return G_SOURCE_REMOVE; + } + + set_value (self, LERP (self->value_from, self->value_to, interpolate (self->type, t))); + + return G_SOURCE_CONTINUE; +} + +static void +phosh_animation_free (PhoshAnimation *self) +{ + phosh_animation_stop (self); + + g_slice_free (PhoshAnimation, self); +} + +/** + * phosh_animation_new: + * @widget: A widget + * @from: The animation's start value + * @to: The animation's end value + * @duration: The duration of the animation in milliseconds + * @type: The type of animation + * @value_cb:(scope forever): The callback applying `value` + * @done_cb:(scope forever): The callback invoked when the animation is done + * @user_data: user_data passed to `value_cb` and `done_cb` + * + * Get a new animation object for @widget. + * + * Note that the scope of the `value_cb` and `done_cb` callbacks is + * actually as long as the animation exists. + * + * Returns:(transfer full): The animation + */ +PhoshAnimation * +phosh_animation_new (GtkWidget *widget, + double from, + double to, + gint64 duration, + PhoshAnimationType type, + PhoshAnimationValueCallback value_cb, + PhoshAnimationDoneCallback done_cb, + gpointer user_data) +{ + PhoshAnimation *self; + + g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL); + g_return_val_if_fail (value_cb != NULL, NULL); + g_return_val_if_fail (done_cb != NULL, NULL); + + self = g_slice_new0 (PhoshAnimation); + + g_atomic_ref_count_init (&self->ref_count); + + self->widget = widget; + self->value_from = from; + self->value_to = to; + self->duration = duration; + self->type = type; + self->value_cb = value_cb; + self->done_cb = done_cb; + self->user_data = user_data; + + self->value = from; + + return self; +} + +PhoshAnimation * +phosh_animation_ref (PhoshAnimation *self) +{ + g_return_val_if_fail (self != NULL, NULL); + g_return_val_if_fail (self->ref_count, NULL); + + g_atomic_ref_count_inc (&self->ref_count); + + return self; +} + +void +phosh_animation_unref (PhoshAnimation *self) +{ + g_return_if_fail (self != NULL); + g_return_if_fail (self->ref_count); + + if (g_atomic_ref_count_dec (&self->ref_count)) + phosh_animation_free (self); +} + +void +phosh_animation_start (PhoshAnimation *self) +{ + g_return_if_fail (self != NULL); + + if (!hdy_get_enable_animations (self->widget) || + !gtk_widget_get_mapped (self->widget) || + self->duration <= 0) { + set_value (self, self->value_to); + + self->done_cb (self->user_data); + + return; + } + + if (self->tick_cb_id) + gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id); + else + g_signal_connect_swapped (self->widget, "unmap", + G_CALLBACK (phosh_animation_stop), self); + + self->start_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (self->widget)) / 1000; + self->tick_cb_id = gtk_widget_add_tick_callback (self->widget, (GtkTickCallback) tick_cb, self, NULL); +} + +void +phosh_animation_stop (PhoshAnimation *self) +{ + g_return_if_fail (self != NULL); + + if (!self->tick_cb_id) + return; + + gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id); + self->tick_cb_id = 0; + + g_signal_handlers_disconnect_by_func (self->widget, phosh_animation_stop, self); + + self->done_cb (self->user_data); +} + +gdouble +phosh_animation_get_value (PhoshAnimation *self) +{ + g_return_val_if_fail (self != NULL, 0.0); + + return self->value; +} diff --git a/src/animation.h b/src/animation.h new file mode 100644 index 000000000..30ced322c --- /dev/null +++ b/src/animation.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_ANIMATION (phosh_animation_get_type()) + +/** + * PhoshAnimationType: + * @PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC: Use ease out cubic interpolation. + * @PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC: Use ease in quintic interpolation. + * @PHOSH_ANIMATION_TYPE_EASE_OUT_QUINTIC: Use ease out quintic interpolation. + * @PHOSH_ANIMATION_TYPE_EASE_OUT_BOUNCE: Use easeOutBounce interpolation. + * + * The animation type of #PhoshAnimationType. + */ +typedef enum { + PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC, + PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC, + PHOSH_ANIMATION_TYPE_EASE_OUT_QUINTIC, + PHOSH_ANIMATION_TYPE_EASE_OUT_BOUNCE, +} PhoshAnimationType; + +typedef struct _PhoshAnimation PhoshAnimation; + +typedef void (*PhoshAnimationValueCallback) (double value, + gpointer user_data); +typedef void (*PhoshAnimationDoneCallback) (gpointer user_data); + +GType phosh_animation_get_type (void) G_GNUC_CONST; + +PhoshAnimation *phosh_animation_new (GtkWidget *widget, + double from, + double to, + gint64 duration, + PhoshAnimationType type, + PhoshAnimationValueCallback value_cb, + PhoshAnimationDoneCallback done_cb, + gpointer user_data); + +PhoshAnimation *phosh_animation_ref (PhoshAnimation *self); +void phosh_animation_unref (PhoshAnimation *self); + +void phosh_animation_start (PhoshAnimation *self); +void phosh_animation_stop (PhoshAnimation *self); + +double phosh_animation_get_value (PhoshAnimation *self); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshAnimation, phosh_animation_unref) + +G_END_DECLS diff --git a/src/app-auth-prompt.c b/src/app-auth-prompt.c new file mode 100644 index 000000000..19dadd320 --- /dev/null +++ b/src/app-auth-prompt.c @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-app-auth-prompt" + +#include "phosh-config.h" + +#include "app-auth-prompt.h" +#include "auth-prompt-option.h" + +#include +#include + +/** + * PhoshAppAuthPrompt: + * + * A system modal prompt to authorize applications + * + * The #PhoshAppAuthPrompt is used to authorize applications. It's used + * by the #PhoshLocationManager and for org.freedesktop.impl.Access. + */ + +enum { + CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +enum { + PROP_0, + + PROP_ICON, + PROP_SUBTITLE, + PROP_BODY, + PROP_DENY_LABEL, + PROP_GRANT_LABEL, + PROP_OFFER_REMEMBER, + PROP_CHOICES, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +typedef struct _PhoshAppAuthPrompt { + PhoshSystemModalDialog parent; + + GIcon *icon; + char *subtitle; + char *body; + char *deny_label; + char *grant_label; + gboolean offer_remember; + GVariant *choices; + + GtkWidget *icon_app; + GtkWidget *lbl_subtitle; + GtkWidget *lbl_body; + GtkWidget *btn_grant; + GtkWidget *btn_deny; + GtkWidget *checkbtn_remember; + GtkWidget *list_box_choices; + + gboolean grant_access; + gboolean remember; +} PhoshAppAuthPrompt; + + +G_DEFINE_TYPE (PhoshAppAuthPrompt, phosh_app_auth_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG) + +#define CHOICE_FORMAT "(ssa(ss)s)" +#define OPTION_FORMAT "(ss)" + +static void +phosh_app_auth_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAppAuthPrompt *self = PHOSH_APP_AUTH_PROMPT (obj); + + switch (prop_id) { + case PROP_ICON: + self->icon = g_value_dup_object (value); + break; + case PROP_SUBTITLE: + self->subtitle = g_value_dup_string (value); + break; + case PROP_BODY: + self->body = g_value_dup_string (value); + break; + case PROP_GRANT_LABEL: + self->grant_label = g_value_dup_string (value); + break; + case PROP_DENY_LABEL: + self->deny_label = g_value_dup_string (value); + break; + case PROP_OFFER_REMEMBER: + self->offer_remember = g_value_get_boolean (value); + break; + case PROP_CHOICES: + self->choices = g_value_dup_variant (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_app_auth_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAppAuthPrompt *self = PHOSH_APP_AUTH_PROMPT (obj); + + switch (prop_id) { + case PROP_ICON: + g_value_set_object (value, self->icon); + break; + case PROP_SUBTITLE: + g_value_set_string (value, self->subtitle); + break; + case PROP_BODY: + g_value_set_string (value, self->body); + break; + case PROP_GRANT_LABEL: + g_value_set_string (value, self->grant_label); + break; + case PROP_DENY_LABEL: + g_value_set_string (value, self->deny_label); + break; + case PROP_OFFER_REMEMBER: + g_value_set_boolean (value, self->offer_remember); + break; + case PROP_CHOICES: + g_value_set_variant (value, self->choices); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +on_btn_grant_clicked (PhoshAppAuthPrompt *self, GtkButton *btn) +{ + self->grant_access = TRUE; + self->remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->checkbtn_remember)); + + g_signal_emit (self, signals[CLOSED], 0); + phosh_system_modal_dialog_close (PHOSH_SYSTEM_MODAL_DIALOG (self)); +} + + +static void +on_dialog_canceled (PhoshAppAuthPrompt *self) +{ + g_return_if_fail (PHOSH_IS_APP_AUTH_PROMPT (self)); + self->remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (self->checkbtn_remember)); + + g_signal_emit (self, signals[CLOSED], 0); + phosh_system_modal_dialog_close (PHOSH_SYSTEM_MODAL_DIALOG (self)); +} + + +static void +add_switch_option ( PhoshAppAuthPrompt *self, + char *choice_id, + char *choice_label, + char *default_option_id) +{ + GtkWidget *action_row_choice; + GtkWidget *switch_choice; + + action_row_choice = g_object_new (HDY_TYPE_ACTION_ROW, + "visible", TRUE, + "title", choice_label, + "activatable", TRUE, + "selectable", FALSE, + NULL); + g_object_set_data_full (G_OBJECT (action_row_choice), "choice-id", g_strdup (choice_id), g_free); + gtk_widget_set_visible (action_row_choice, TRUE); + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (action_row_choice), choice_label); + + switch_choice = g_object_new (GTK_TYPE_SWITCH, + "visible", TRUE, + "halign", GTK_ALIGN_CENTER, + "valign", GTK_ALIGN_CENTER, + NULL); + hdy_action_row_set_activatable_widget (HDY_ACTION_ROW (action_row_choice), switch_choice); + gtk_container_add (GTK_CONTAINER (action_row_choice), switch_choice); + + gtk_container_add (GTK_CONTAINER (self->list_box_choices), action_row_choice); +} + +static char * +get_choice_option_name (gpointer item, gpointer unused) { + PhoshAuthPromptOption *option = PHOSH_AUTH_PROMPT_OPTION (item); + return g_strdup (phosh_auth_prompt_option_get_label (option)); +} + +static void +add_combo_option (PhoshAppAuthPrompt *self, + char *choice_id, + char *choice_label, + GVariantIter *iter_options, + char *default_option_id) +{ + int index = 0; + int selected_index = 0; + char *option_id; + char *option_label; + GListStore *store; + GtkWidget *combo_row_options; + + combo_row_options = hdy_combo_row_new (); + g_object_set_data_full (G_OBJECT (combo_row_options), "choice-id", g_strdup (choice_id), g_free); + gtk_widget_set_visible (combo_row_options, TRUE); + hdy_preferences_row_set_title (HDY_PREFERENCES_ROW (combo_row_options), choice_label); + gtk_list_box_row_set_activatable (GTK_LIST_BOX_ROW (combo_row_options), TRUE); + gtk_list_box_row_set_selectable (GTK_LIST_BOX_ROW (combo_row_options), FALSE); + + store = g_list_store_new (PHOSH_TYPE_AUTH_PROMPT_OPTION); + while (g_variant_iter_loop (iter_options, OPTION_FORMAT, &option_id, &option_label)) { + GObject *option; + option = g_object_new (PHOSH_TYPE_AUTH_PROMPT_OPTION, + "id", option_id, + "label", option_label, + NULL); + if (strcmp (option_id, default_option_id) == 0) { + selected_index = index; + } + g_list_store_append(store, option); + g_object_unref (option); + index += 1; + } + hdy_combo_row_bind_name_model (HDY_COMBO_ROW (combo_row_options), + G_LIST_MODEL (store), + get_choice_option_name, + NULL, + NULL); + hdy_combo_row_set_selected_index (HDY_COMBO_ROW (combo_row_options), selected_index); + gtk_container_add (GTK_CONTAINER (self->list_box_choices), combo_row_options); +} + + +static void +add_choice_to_gvariant (GtkWidget *child, gpointer builder) +{ + GVariant *choice[2]; + + choice[0] = g_variant_new_string (g_object_get_data (G_OBJECT (child), "choice-id")); + if (HDY_IS_COMBO_ROW (child)) { + HdyComboRow *row = HDY_COMBO_ROW (child); + GListModel *model = hdy_combo_row_get_model (row); + gint selected_index = hdy_combo_row_get_selected_index (row); + PhoshAuthPromptOption *option = (PhoshAuthPromptOption*) g_list_model_get_item (model, selected_index); + + if (option != NULL) { + choice[1] = g_variant_new_string (phosh_auth_prompt_option_get_id (option)); + } + } else { + HdyActionRow *row = HDY_ACTION_ROW (child); + GtkSwitch *gtk_switch = GTK_SWITCH (hdy_action_row_get_activatable_widget (row)); + + choice[1] = g_variant_new_string (gtk_switch_get_state(gtk_switch) ? "true" : "false"); + } + g_variant_builder_add_value ((GVariantBuilder*) builder, g_variant_new_tuple (choice, 2)); +} + + +static void +phosh_app_auth_prompt_finalize (GObject *obj) +{ + PhoshAppAuthPrompt *self = PHOSH_APP_AUTH_PROMPT (obj); + + g_clear_object (&self->icon); + g_clear_pointer (&self->subtitle, g_free); + g_clear_pointer (&self->body, g_free); + g_clear_pointer (&self->grant_label, g_free); + g_clear_pointer (&self->deny_label, g_free); + + G_OBJECT_CLASS (phosh_app_auth_prompt_parent_class)->finalize (obj); +} + + +static void +phosh_app_auth_prompt_constructed (GObject *object) +{ + PhoshAppAuthPrompt *self = PHOSH_APP_AUTH_PROMPT (object); + + G_OBJECT_CLASS (phosh_app_auth_prompt_parent_class)->constructed (object); + + gtk_widget_grab_default (self->btn_grant); + + if (self->choices != NULL) { + GVariantIter iter_choices; + char *choice_id; + char *choice_label; + g_autoptr (GVariantIter) iter_options = NULL; + char *default_option_id; + + g_variant_iter_init (&iter_choices, self->choices); + + if (g_variant_iter_n_children (&iter_choices) == 0) { + gtk_widget_set_visible (self->list_box_choices, FALSE); + } + + while (g_variant_iter_loop (&iter_choices, CHOICE_FORMAT, + &choice_id, + &choice_label, + &iter_options, + &default_option_id)) { + if (g_variant_iter_n_children (iter_options) == 0) { + add_switch_option (self, choice_id, choice_label, default_option_id); + } else { + add_combo_option (self, choice_id, choice_label, iter_options, default_option_id); + } + } + } +} + + +static void +phosh_app_auth_prompt_class_init (PhoshAppAuthPromptClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_app_auth_prompt_get_property; + object_class->set_property = phosh_app_auth_prompt_set_property; + object_class->constructed = phosh_app_auth_prompt_constructed; + object_class->finalize = phosh_app_auth_prompt_finalize; + + props[PROP_ICON] = + g_param_spec_object ( + "icon", + "Icon", + "The auth dialog icon", + G_TYPE_ICON, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_SUBTITLE] = + g_param_spec_string ( + "subtitle", + "Subtitle", + "The auth dialog subtitle", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_BODY] = + g_param_spec_string ( + "body", + "Body", + "The auth dialog body", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_GRANT_LABEL] = + g_param_spec_string ( + "grant-label", + "Grant label", + "The auth dialog's grant access button label", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_DENY_LABEL] = + g_param_spec_string ( + "deny-label", + "Deny label", + "The auth dialog's deny access button label", + "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_OFFER_REMEMBER] = + g_param_spec_boolean ( + "offer-remember", + "Offer Remember", + "Whether to offer to remember the auth decision result", + FALSE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_CHOICES] = + g_param_spec_variant ( + "choices", + "Choices", + "The dialogs shown permissions and their possible values", + G_VARIANT_TYPE (PHOSH_APP_AUTH_PROMPT_CHOICES_FORMAT), + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/app-auth-prompt.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, icon_app); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, lbl_subtitle); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, lbl_body); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, btn_grant); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, btn_deny); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, checkbtn_remember); + gtk_widget_class_bind_template_child (widget_class, PhoshAppAuthPrompt, list_box_choices); + gtk_widget_class_bind_template_callback (widget_class, on_btn_grant_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); +} + + +static void +phosh_app_auth_prompt_init (PhoshAppAuthPrompt *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property (self, "subtitle", self->lbl_subtitle, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "body", self->lbl_body, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "grant-label", self->btn_grant, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "deny-label", self->btn_deny, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "icon", self->icon_app, "gicon", G_BINDING_DEFAULT); + g_object_bind_property (self, "offer-remember", self->checkbtn_remember, "visible", G_BINDING_DEFAULT); +} + + +GtkWidget * +phosh_app_auth_prompt_new (GIcon *icon, + const char *title, + const char *subtitle, + const char *body, + const char *grant_label, + const char *deny_label, + gboolean offer_remember, + GVariant *choices) +{ + return g_object_new (PHOSH_TYPE_APP_AUTH_PROMPT, + "icon", icon, + "title", title, + "subtitle", subtitle, + "body", body, + "grant-label", grant_label, + "deny-label", deny_label, + "offer-remember", offer_remember, + "choices", choices, + NULL); +} + +gboolean +phosh_app_auth_prompt_get_grant_access (GtkWidget *self) +{ + g_return_val_if_fail (PHOSH_IS_APP_AUTH_PROMPT (self), FALSE); + + return PHOSH_APP_AUTH_PROMPT (self)->grant_access; +} + + +GVariant* phosh_app_auth_prompt_get_selected_choices (GtkWidget *self) +{ + GVariantBuilder builder; + + g_return_val_if_fail (PHOSH_IS_APP_AUTH_PROMPT (self), NULL); + g_variant_builder_init (&builder, G_VARIANT_TYPE ("a(ss)")); + gtk_container_foreach ( + GTK_CONTAINER ( PHOSH_APP_AUTH_PROMPT (self)->list_box_choices), + add_choice_to_gvariant, + &builder + ); + return g_variant_builder_end (&builder); +} diff --git a/src/app-auth-prompt.h b/src/app-auth-prompt.h new file mode 100644 index 000000000..eccf3709c --- /dev/null +++ b/src/app-auth-prompt.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +#define PHOSH_APP_AUTH_PROMPT_CHOICES_FORMAT "a(ssa(ss)s)" + +#define PHOSH_TYPE_APP_AUTH_PROMPT (phosh_app_auth_prompt_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAppAuthPrompt, phosh_app_auth_prompt, PHOSH, APP_AUTH_PROMPT, PhoshSystemModalDialog) + +GtkWidget *phosh_app_auth_prompt_new (GIcon *icon, + const char *title, + const char *subtitle, + const char *body, + const char *grant_label, + const char *deny_label, + gboolean offer_remember, + GVariant *choices); +gboolean phosh_app_auth_prompt_get_grant_access (GtkWidget *self); +GVariant* phosh_app_auth_prompt_get_selected_choices (GtkWidget *self); +gboolean phosh_app_auth_prompt_get_remember (GtkWidget *self); + +G_END_DECLS diff --git a/src/app-grid-base-button.c b/src/app-grid-base-button.c new file mode 100644 index 000000000..bc6b31ac1 --- /dev/null +++ b/src/app-grid-base-button.c @@ -0,0 +1,249 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-app-grid-base-button" + +#include "app-grid-base-button.h" + +#include "fading-label.h" +#include "clamp.h" + +/** + * PhoshAppGridBaseButton: + * + * Base class for buttons in app grid. Add the display widget (like image or grid of images) as a + * child. Use [property@Phosh.AppGridBaseButton:label] property to set the label. + */ + +enum { + PROP_0, + PROP_LABEL, + PROP_CHILD, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct { + GtkBox *box; + GtkWidget *button; + PhoshFadingLabel *label; + GtkWidget *child; +} PhoshAppGridBaseButtonPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (PhoshAppGridBaseButton, phosh_app_grid_base_button, + GTK_TYPE_FLOW_BOX_CHILD); + + +static void +phosh_app_grid_base_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridBaseButton *self = PHOSH_APP_GRID_BASE_BUTTON (object); + + switch (property_id) { + case PROP_LABEL: + phosh_app_grid_base_button_set_label (self, g_value_get_string (value)); + break; + case PROP_CHILD: + phosh_app_grid_base_button_set_child (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_app_grid_base_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridBaseButton *self = PHOSH_APP_GRID_BASE_BUTTON (object); + + switch (property_id) { + case PROP_LABEL: + g_value_set_string (value, phosh_app_grid_base_button_get_label (self)); + break; + case PROP_CHILD: + g_value_set_object (value, phosh_app_grid_base_button_get_child (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +on_clicked_cb (PhoshAppGridBaseButton *self) +{ + g_signal_emit_by_name (self, "activate", NULL); +} + + +static void +phosh_app_grid_base_button_destroy (GtkWidget *widget) +{ + PhoshAppGridBaseButton *self = PHOSH_APP_GRID_BASE_BUTTON (widget); + + phosh_app_grid_base_button_set_child (self, NULL); + + GTK_WIDGET_CLASS (phosh_app_grid_base_button_parent_class)->destroy (widget); +} + + +static void +phosh_app_grid_base_button_grab_focus (GtkWidget *widget) +{ + PhoshAppGridBaseButton *self = PHOSH_APP_GRID_BASE_BUTTON (widget); + PhoshAppGridBaseButtonPrivate *priv = phosh_app_grid_base_button_get_instance_private (self); + + gtk_widget_grab_focus (priv->button); +} + + +static void +phosh_app_grid_base_button_class_init (PhoshAppGridBaseButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_app_grid_base_button_set_property; + object_class->get_property = phosh_app_grid_base_button_get_property; + + widget_class->destroy = phosh_app_grid_base_button_destroy; + widget_class->grab_focus = phosh_app_grid_base_button_grab_focus; + + /** + * PhoshAppGridBaseButton:label: + * + * The label to display for button. A `NULL` results in the label widget being hidden. + */ + props[PROP_LABEL] = + g_param_spec_string ("label", "", "", + NULL, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * PhoshAppGridBaseButton:child: + * + * The child content of the button. + */ + props[PROP_CHILD] = + g_param_spec_object ("child", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + g_type_ensure (PHOSH_TYPE_CLAMP); + g_type_ensure (PHOSH_TYPE_FADING_LABEL); + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/app-grid-base-button.ui"); + + gtk_widget_class_bind_template_callback (widget_class, on_clicked_cb); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridBaseButton, box); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridBaseButton, button); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridBaseButton, label); +} + + +static void +phosh_app_grid_base_button_init (PhoshAppGridBaseButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +void +phosh_app_grid_base_button_set_label (PhoshAppGridBaseButton *self, const char *label) +{ + PhoshAppGridBaseButtonPrivate *priv; + + g_return_if_fail (PHOSH_IS_APP_GRID_BASE_BUTTON (self)); + + priv = phosh_app_grid_base_button_get_instance_private (self); + + if (g_strcmp0 (label, phosh_fading_label_get_label (priv->label)) == 0) + return; + + phosh_fading_label_set_label (priv->label, label); + gtk_widget_set_visible (GTK_WIDGET (priv->label), label != NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]); +} + + +const char * +phosh_app_grid_base_button_get_label (PhoshAppGridBaseButton *self) +{ + PhoshAppGridBaseButtonPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_APP_GRID_BASE_BUTTON (self), 0); + + priv = phosh_app_grid_base_button_get_instance_private (self); + + return phosh_fading_label_get_label (priv->label); +} + +/** + * phosh_app_grid_base_button_set_child: + * @self: An app-grid base button + * @child: A widget or `NULL` + * + * Set the child of base button. Use `NULL` to remove exisitng child. + */ +void +phosh_app_grid_base_button_set_child (PhoshAppGridBaseButton *self, GtkWidget *child) +{ + PhoshAppGridBaseButtonPrivate *priv; + + g_return_if_fail (PHOSH_IS_APP_GRID_BASE_BUTTON (self)); + g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); + + priv = phosh_app_grid_base_button_get_instance_private (self); + + if (priv->child == child) + return; + + if (priv->child) + gtk_container_remove (GTK_CONTAINER (priv->box), priv->child); + + priv->child = child; + + if (priv->child) { + gtk_box_pack_start (priv->box, priv->child, FALSE, FALSE, 0); + gtk_box_reorder_child (priv->box, priv->child, 0); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]); +} + +/** + * phosh_app_grid_base_button_get_child: + * @self: An app-grid base button + * + * Get the current child of base button. + * + * Returns:(transfer none): The child or `NULL`. + */ +GtkWidget * +phosh_app_grid_base_button_get_child (PhoshAppGridBaseButton *self) +{ + PhoshAppGridBaseButtonPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_APP_GRID_BASE_BUTTON (self), NULL); + + priv = phosh_app_grid_base_button_get_instance_private (self); + + return priv->child; +} diff --git a/src/app-grid-base-button.h b/src/app-grid-base-button.h new file mode 100644 index 000000000..c10689d40 --- /dev/null +++ b/src/app-grid-base-button.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_APP_GRID_BASE_BUTTON (phosh_app_grid_base_button_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshAppGridBaseButton, phosh_app_grid_base_button, PHOSH, APP_GRID_BASE_BUTTON, GtkFlowBoxChild) + +struct _PhoshAppGridBaseButtonClass +{ + GtkFlowBoxChildClass parent_class; +}; + +void phosh_app_grid_base_button_set_label (PhoshAppGridBaseButton *self, const char *label); +const char *phosh_app_grid_base_button_get_label (PhoshAppGridBaseButton *self); +void phosh_app_grid_base_button_set_child (PhoshAppGridBaseButton *self, GtkWidget *child); +GtkWidget *phosh_app_grid_base_button_get_child (PhoshAppGridBaseButton *self); + +G_END_DECLS diff --git a/src/app-grid-button.c b/src/app-grid-button.c new file mode 100644 index 000000000..9c9ced2f5 --- /dev/null +++ b/src/app-grid-button.c @@ -0,0 +1,799 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "phosh-app-grid-button" + +#include "phosh-config.h" +#include "app-tracker.h" +#include "app-grid-button.h" +#include "clamp.h" +#include "fading-label.h" +#include "metainfo-cache.h" +#include "phosh-enums.h" +#include "favorite-list-model.h" +#include "util.h" + +#include "shell-priv.h" +#include "util.h" + +/** + * PhoshAppGridButton: + * + * An app-grid button to represent an application launcher or favorite. + */ + +typedef struct _PhoshAppGridButtonPrivate PhoshAppGridButtonPrivate; +struct _PhoshAppGridButtonPrivate { + GAppInfo *info; + gboolean is_favorite; + PhoshAppGridButtonMode mode; + PhoshFolderInfo *folder_info; + + gulong favorite_changed_watcher; + + GtkWidget *icon; + GtkWidget *popover; + GtkGesture *long_gesture; + GtkGesture *right_gesture; + + GMenu *menu; + GMenu *actions; + GMenu *folders; + + GActionMap *action_map; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshAppGridButton, phosh_app_grid_button, + PHOSH_TYPE_APP_GRID_BASE_BUTTON) + +enum { + PROP_0, + PROP_APP_INFO, + PROP_IS_FAVORITE, + PROP_MODE, + PROP_FOLDER_INFO, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + +enum { + APP_LAUNCHED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +static void +phosh_app_grid_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (object); + + switch (property_id) { + case PROP_APP_INFO: + phosh_app_grid_button_set_app_info (self, g_value_get_object (value)); + break; + case PROP_MODE: + phosh_app_grid_button_set_mode (self, g_value_get_enum (value)); + break; + case PROP_FOLDER_INFO: + phosh_app_grid_button_set_folder_info (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_app_grid_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (object); + + switch (property_id) { + case PROP_APP_INFO: + g_value_set_object (value, phosh_app_grid_button_get_app_info (self)); + break; + case PROP_IS_FAVORITE: + g_value_set_boolean (value, phosh_app_grid_button_is_favorite (self)); + break; + case PROP_MODE: + g_value_set_enum (value, phosh_app_grid_button_get_mode (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_app_grid_button_finalize (GObject *object) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (object); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + g_clear_object (&priv->info); + g_clear_object (&priv->menu); + g_clear_object (&priv->actions); + g_clear_object (&priv->action_map); + g_clear_object (&priv->folder_info); + + g_clear_signal_handler (&priv->favorite_changed_watcher, + phosh_favorite_list_model_get_default ()); + + G_OBJECT_CLASS (phosh_app_grid_button_parent_class)->finalize (object); +} + + +static void +populate_folders_menu (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autoptr (GSettings) settings = NULL; + g_auto (GStrv) folder_paths = NULL; + g_autoptr (GMenu) submenu = g_menu_new (); + g_autoptr (GMenuItem) submenu_item = g_menu_item_new_submenu (_("Add to Folder"), + G_MENU_MODEL (submenu)); + g_autoptr (GMenu) folders_section = g_menu_new (); + g_autoptr (GMenu) new_folder_section = g_menu_new (); + + settings = g_settings_new (PHOSH_FOLDERS_SCHEMA_ID); + folder_paths = g_settings_get_strv (settings, "folder-children"); + + for (int i = 0; folder_paths[i] != NULL; i++) { + g_autoptr (PhoshFolderInfo) folder = phosh_folder_info_new_from_folder_path (folder_paths[i]); + g_autofree char *detailed_action = NULL; + + if (!g_app_info_should_show (G_APP_INFO (folder))) + continue; + + if (priv->folder_info != NULL && + g_app_info_equal (G_APP_INFO (folder), G_APP_INFO (priv->folder_info))) + continue; + + detailed_action = g_strdup_printf ("app-btn.folder-add::%s", folder_paths[i]); + g_menu_append (folders_section, phosh_folder_info_get_name (folder), detailed_action); + } + + g_menu_append_section (submenu, NULL, G_MENU_MODEL (folders_section)); + g_menu_append (new_folder_section, _("Create new folder"), "app-btn.folder-new"); + g_menu_append_section (submenu, NULL, G_MENU_MODEL (new_folder_section)); + + g_menu_append_item (priv->folders, submenu_item); +} + + +static void +context_menu (GtkWidget *widget, + GdkEvent *event) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (widget); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + g_menu_remove_all (priv->folders); + if (!priv->is_favorite && priv->folder_info == NULL) + populate_folders_menu (self); + gtk_popover_popup (GTK_POPOVER (priv->popover)); +} + + +static void +on_right_pressed (GtkWidget *self, int n_press, double x, double y, GtkGesture *gesture) +{ + const GdkEvent *event = gtk_gesture_get_last_event (gesture, NULL); + if (gdk_event_triggers_context_menu (event)) + context_menu (self, (GdkEvent *) event); +} + +static void +on_long_pressed (GtkWidget *self, double x, double y, GtkGesture *gesture) +{ + context_menu (self, NULL); +} + + + +static void +activate_cb (PhoshAppGridButton *self) +{ + PhoshAppTracker *app_tracker = phosh_shell_get_app_tracker (phosh_shell_get_default ()); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_APP_TRACKER (app_tracker)); + + phosh_app_tracker_launch_app_info (app_tracker, priv->info); + g_signal_emit (self, signals[APP_LAUNCHED], 0, priv->info); +} + + +static void +phosh_app_grid_button_class_init (PhoshAppGridButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_app_grid_button_set_property; + object_class->get_property = phosh_app_grid_button_get_property; + object_class->finalize = phosh_app_grid_button_finalize; + + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", "App", "App Info", + G_TYPE_APP_INFO, + G_PARAM_STATIC_STRINGS | + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshAppGridButton:is-favorite: + * + * %TRUE when the application is currently favorited + * + * Stability: Private + */ + props[PROP_IS_FAVORITE] = + g_param_spec_boolean ("is-favorite", "Favorite", "Is a favorite app", + FALSE, + G_PARAM_STATIC_STRINGS | + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshAppGridButton:mode: + * + * The #PhoshAppGridButtonMode of the button + * + * In %PHOSH_APP_GRID_BUTTON_FAVORITES the label is + * hidden + * + * Stability: Private + */ + props[PROP_MODE] = + g_param_spec_enum ("mode", "Mode", "Button mode", + PHOSH_TYPE_APP_GRID_BUTTON_MODE, + PHOSH_APP_GRID_BUTTON_LAUNCHER, + G_PARAM_STATIC_STRINGS | + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshAppGridButton:folder-info: + * + * The folder-info to which the button is a part of. Can be NULL. + * + * Stability: Private + */ + props[PROP_FOLDER_INFO] = + g_param_spec_object ("folder-info", "", "", + PHOSH_TYPE_FOLDER_INFO, + G_PARAM_WRITABLE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/app-grid-button.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, icon); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, popover); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, long_gesture); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, right_gesture); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, menu); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, actions); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGridButton, folders); + + gtk_widget_class_bind_template_callback (widget_class, on_long_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_right_pressed); + gtk_widget_class_bind_template_callback (widget_class, activate_cb); + + signals[APP_LAUNCHED] = g_signal_new ("app-launched", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_APP_INFO); + + gtk_widget_class_set_css_name (widget_class, "phosh-app-grid-button"); +} + + +static void +action_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autoptr (GdkAppLaunchContext) context = NULL; + const char *action_name; + + action_name = g_variant_get_string (parameter, NULL); + + g_debug ("Launching %s->%s", g_app_info_get_id (priv->info), action_name); + + g_return_if_fail (action_name != NULL); + g_return_if_fail (G_IS_DESKTOP_APP_INFO (priv->info)); + + context = gdk_display_get_app_launch_context (gtk_widget_get_display (GTK_WIDGET (self))); + + g_desktop_app_info_launch_action (G_DESKTOP_APP_INFO (priv->info), + action_name, + G_APP_LAUNCH_CONTEXT (context)); + + g_signal_emit (self, signals[APP_LAUNCHED], 0, priv->info); +} + + +static void +favorite_remove_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + phosh_favorite_list_model_remove_app (NULL, priv->info); +} + + +static void +favorite_add_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + + phosh_favorite_list_model_add_app (NULL, priv->info); +} + + +static void +spawn_gnome_software (const char *action, const char *app_id) +{ + const char *argv[] = { "gnome-software", action, app_id, NULL }; + g_autoptr (GError) err = NULL; + + if (!g_spawn_async (NULL, (char **)argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL, NULL, &err)) + g_warning ("Failed to run 'gnome-software %s' for '%s': %s", action, app_id, err->message); +} + + +static void +view_details_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + const char *app_id = g_app_info_get_id (priv->info); + g_return_if_fail (app_id); + + spawn_gnome_software ("--details", app_id); +} + + +static void +uninstall_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + g_autofree char *appstream_id = NULL; + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + const char *app_id = g_app_info_get_id (priv->info); + g_return_if_fail (app_id); + + appstream_id = phosh_metainfo_get_data_id (phosh_metainfo_cache_get_default (), app_id); + g_return_if_fail (appstream_id); + + spawn_gnome_software ("--uninstall", appstream_id); +} + +static void +add_to_folder_children (char *folder_path) +{ + g_autoptr (GSettings) settings = g_settings_new (PHOSH_FOLDERS_SCHEMA_ID); + g_auto (GStrv) folders = NULL; + g_auto (GStrv) new_folders = NULL; + + folders = g_settings_get_strv (settings, "folder-children"); + new_folders = phosh_util_append_to_strv (folders, folder_path); + g_settings_set_strv (settings, "folder-children", (const char *const *) new_folders); +} + + +static void +remove_from_folder_children (char *folder_path) +{ + g_autoptr (GSettings) settings = g_settings_new (PHOSH_FOLDERS_SCHEMA_ID); + g_auto (GStrv) folders = NULL; + g_auto (GStrv) new_folders = NULL; + + folders = g_settings_get_strv (settings, "folder-children"); + new_folders = phosh_util_remove_from_strv (folders, folder_path); + g_settings_set_strv (settings, "folder-children", (const char *const *) new_folders); +} + + +static void +remove_from_folder (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autofree char *folder_path; + + g_object_get (priv->folder_info, "path", &folder_path, NULL); + + if (!phosh_folder_info_remove_app_info (priv->folder_info, priv->info)) + remove_from_folder_children (folder_path); +} + + +static void +add_to_folder (PhoshAppGridButton *self, PhoshFolderInfo *folder_info) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + /* If the app-info gets removed, then the change causes the grid to be filled with new buttons. + * This would cause the button and in-turn the app-info to be disposed. Take a reference to avoid + * that and clear it when we are done. */ + g_autoptr (GAppInfo) info = g_object_ref (priv->info); + + if (priv->folder_info) + remove_from_folder (self); + phosh_folder_info_add_app_info (folder_info, info); +} + + +static void +folder_add_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + char *folder_path = (char *) g_variant_get_string (parameter, NULL); + g_autoptr (PhoshFolderInfo) folder_info = phosh_folder_info_new_from_folder_path (folder_path); + + add_to_folder (self, folder_info); +} + + +static void +folder_new_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + g_autofree char *folder_path = g_uuid_string_random (); + g_autoptr (PhoshFolderInfo) folder_info = phosh_folder_info_new_from_folder_path (folder_path); + + phosh_folder_info_set_name (folder_info, g_app_info_get_name (priv->info)); + add_to_folder (self, folder_info); + add_to_folder_children (folder_path); +} + + +static void +folder_remove_activated (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshAppGridButton *self = PHOSH_APP_GRID_BUTTON (data); + remove_from_folder (self); +} + + +static GActionEntry entries[] = +{ + { .name = "action", .activate = action_activated, .parameter_type = "s" }, + { .name = "favorite-remove", .activate = favorite_remove_activated }, + { .name = "favorite-add", .activate = favorite_add_activated }, + { .name = "view-details", .activate = view_details_activated }, + { .name = "uninstall", .activate = uninstall_activated }, + { .name = "folder-add", .activate = folder_add_activated, .parameter_type = "s" }, + { .name = "folder-new", .activate = folder_new_activated }, + { .name = "folder-remove", .activate = folder_remove_activated }, +}; + + +static void +phosh_app_grid_button_init (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv = phosh_app_grid_button_get_instance_private (self); + PhoshMetainfoCache *metainfo_cache = phosh_metainfo_cache_get_default (); + GAction *act; + gboolean have_gnome_software = FALSE; + + have_gnome_software = phosh_util_have_gnome_software (FALSE); + + priv->is_favorite = FALSE; + priv->mode = PHOSH_APP_GRID_BUTTON_LAUNCHER; + priv->favorite_changed_watcher = 0; + + priv->action_map = G_ACTION_MAP (g_simple_action_group_new ()); + g_action_map_add_action_entries (priv->action_map, + entries, + G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), + "app-btn", + G_ACTION_GROUP (priv->action_map)); + + act = g_action_map_lookup_action (priv->action_map, "favorite-add"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), TRUE); + act = g_action_map_lookup_action (priv->action_map, "favorite-remove"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), FALSE); + act = g_action_map_lookup_action (priv->action_map, "view-details"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), have_gnome_software); + act = g_action_map_lookup_action (priv->action_map, "folder-remove"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), FALSE); + + /* Uninstall is hidden initially until we're sure the AppStream cache has loaded. */ + act = g_action_map_lookup_action (priv->action_map, "uninstall"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), FALSE); + + if (have_gnome_software) { + act = g_action_map_lookup_action (priv->action_map, "uninstall"); + g_object_bind_property (metainfo_cache, "ready", act, "enabled", G_BINDING_SYNC_CREATE); + } + + g_type_ensure (PHOSH_TYPE_CLAMP); + g_type_ensure (PHOSH_TYPE_FADING_LABEL); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_popover_bind_model (GTK_POPOVER (priv->popover), + G_MENU_MODEL (priv->menu), + NULL); +} + + +GtkWidget * +phosh_app_grid_button_new (GAppInfo *info) +{ + return g_object_new (PHOSH_TYPE_APP_GRID_BUTTON, + "app-info", info, + NULL); +} + + +GtkWidget * +phosh_app_grid_button_new_favorite (GAppInfo *info) +{ + return g_object_new (PHOSH_TYPE_APP_GRID_BUTTON, + "app-info", info, + "mode", PHOSH_APP_GRID_BUTTON_FAVORITES, + NULL); +} + + +static void +favorites_changed (GListModel *list, + guint position, + guint removed, + guint added, + PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv; + gboolean favorite = FALSE; + GAction *act; + + g_return_if_fail (PHOSH_IS_APP_GRID_BUTTON (self)); + g_return_if_fail (PHOSH_IS_FAVORITE_LIST_MODEL (list)); + + priv = phosh_app_grid_button_get_instance_private (self); + + favorite = phosh_favorite_list_model_app_is_favorite (PHOSH_FAVORITE_LIST_MODEL (list), + priv->info); + + if (priv->is_favorite == favorite) + return; + + act = g_action_map_lookup_action (priv->action_map, "favorite-add"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), !favorite); + act = g_action_map_lookup_action (priv->action_map, "favorite-remove"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), favorite); + + priv->is_favorite = favorite; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_FAVORITE]); +} + + +void +phosh_app_grid_button_set_app_info (PhoshAppGridButton *self, + GAppInfo *info) +{ + PhoshFavoriteListModel *list = NULL; + PhoshAppGridButtonPrivate *priv; + GIcon *icon; + const char *name; + + g_return_if_fail (PHOSH_IS_APP_GRID_BUTTON (self)); + g_return_if_fail (G_IS_APP_INFO (info) || info == NULL); + + priv = phosh_app_grid_button_get_instance_private (self); + + if (priv->info == info) + return; + + g_clear_object (&priv->info); + + g_menu_remove_all (priv->actions); + + list = phosh_favorite_list_model_get_default (); + + g_clear_signal_handler (&priv->favorite_changed_watcher, list); + + if (info) { + priv->info = g_object_ref (info); + + priv->favorite_changed_watcher = g_signal_connect (list, + "items-changed", + G_CALLBACK (favorites_changed), + self); + favorites_changed (G_LIST_MODEL (list), 0, 0, 0, self); + + name = g_app_info_get_name (G_APP_INFO (priv->info)); + phosh_app_grid_base_button_set_label (PHOSH_APP_GRID_BASE_BUTTON (self), name); + + icon = g_app_info_get_icon (priv->info); + if (G_UNLIKELY (icon == NULL)) { + gtk_image_set_from_icon_name (GTK_IMAGE (priv->icon), PHOSH_APP_UNKNOWN_ICON, -1); + } else { + if (G_IS_THEMED_ICON (icon)) { + g_themed_icon_append_name (G_THEMED_ICON (icon), + PHOSH_APP_UNKNOWN_ICON); + } + gtk_image_set_from_gicon (GTK_IMAGE (priv->icon), icon, -1); + } + + gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); + + if (G_IS_DESKTOP_APP_INFO (priv->info)) { + const char *const *actions = NULL; + int i = 0; + + actions = g_desktop_app_info_list_actions (G_DESKTOP_APP_INFO (priv->info)); + + /* + * So the dummy GAppInfo for the tests is (for reasons known only to gio) + * actually a GDesktopAppInfo rather than something like GDummyAppInfo, + * this means that guarding this block with G_IS_DESKTOP_APP_INFO + * doesn't actually help much. This seems to surprise even gio as instead + * of always returning at least an empty array (as the API promises) it + * returns NULL + * + * tl;dr: we do (actions && actions[i]) instead of (actions[i]) otherwise + * the tests explode because of a condition that can only exist in + * the tests + */ + + while (actions && actions[i]) { + g_autofree char *detailed_action = NULL; + g_autofree char *label = NULL; + + detailed_action = g_strdup_printf ("app-btn.action::%s", actions[i]); + + label = g_desktop_app_info_get_action_name (G_DESKTOP_APP_INFO (priv->info), + actions[i]); + + g_menu_append (priv->actions, label, detailed_action); + + i++; + } + } + } else { + phosh_app_grid_base_button_set_label (PHOSH_APP_GRID_BASE_BUTTON (self), _("Application")); + gtk_image_set_from_icon_name (GTK_IMAGE (priv->icon), PHOSH_APP_UNKNOWN_ICON, -1); + + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_INFO]); +} + +/** + * phosh_app_grid_button_get_app_info: + * @self: An app grid button + * + * Get the app info + * + * Returns:(transfer none): The app info + */ +GAppInfo * +phosh_app_grid_button_get_app_info (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_APP_GRID_BUTTON (self), NULL); + priv = phosh_app_grid_button_get_instance_private (self); + + return priv->info; +} + + +gboolean +phosh_app_grid_button_is_favorite (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_APP_GRID_BUTTON (self), FALSE); + priv = phosh_app_grid_button_get_instance_private (self); + + return priv->is_favorite; +} + + +void +phosh_app_grid_button_set_mode (PhoshAppGridButton *self, + PhoshAppGridButtonMode mode) +{ + PhoshAppGridButtonPrivate *priv; + const char *name; + + g_return_if_fail (PHOSH_IS_APP_GRID_BUTTON (self)); + priv = phosh_app_grid_button_get_instance_private (self); + + if (priv->mode == mode) + return; + + name = priv->info == NULL ? _("Application") : g_app_info_get_name (priv->info); + + switch (mode) { + case PHOSH_APP_GRID_BUTTON_LAUNCHER: + phosh_app_grid_base_button_set_label (PHOSH_APP_GRID_BASE_BUTTON (self), name); + break; + case PHOSH_APP_GRID_BUTTON_FAVORITES: + phosh_app_grid_base_button_set_label (PHOSH_APP_GRID_BASE_BUTTON (self), NULL); + break; + default: + g_critical ("Invalid mode %i", mode); + return; + } + + priv->mode = mode; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODE]); +} + + +PhoshAppGridButtonMode +phosh_app_grid_button_get_mode (PhoshAppGridButton *self) +{ + PhoshAppGridButtonPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_APP_GRID_BUTTON (self), FALSE); + priv = phosh_app_grid_button_get_instance_private (self); + + return priv->mode; +} + + +void +phosh_app_grid_button_set_folder_info (PhoshAppGridButton *self, PhoshFolderInfo *folder_info) +{ + PhoshAppGridButtonPrivate *priv; + GAction *act; + + g_return_if_fail (PHOSH_IS_APP_GRID_BUTTON (self)); + g_return_if_fail ((folder_info == NULL) || PHOSH_IS_FOLDER_INFO (folder_info)); + priv = phosh_app_grid_button_get_instance_private (self); + + g_clear_object (&priv->folder_info); + act = g_action_map_lookup_action (priv->action_map, "folder-remove"); + + if (folder_info) { + priv->folder_info = g_object_ref (folder_info); + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), TRUE); + } else { + g_simple_action_set_enabled (G_SIMPLE_ACTION (act), FALSE); + } +} diff --git a/src/app-grid-button.h b/src/app-grid-button.h new file mode 100644 index 000000000..eeedb35b7 --- /dev/null +++ b/src/app-grid-button.h @@ -0,0 +1,59 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "app-grid-base-button.h" +#include "folder-info.h" + +#include +#include +#include + +#pragma once + +G_BEGIN_DECLS + + +/** + * PHOSH_APP_UNKNOWN_ICON: + * + * Icon name to use for apps we can't identify or whose icon is invalid + */ +#define PHOSH_APP_UNKNOWN_ICON "app-icon-unknown" + +#define PHOSH_TYPE_APP_GRID_BUTTON phosh_app_grid_button_get_type () + +/** + * PhoshAppGridButtonMode: + * @PHOSH_APP_GRID_BUTTON_LAUNCHER: Standard mode used in drawer/search etc + * @PHOSH_APP_GRID_BUTTON_FAVORITES: Fovourites area (doesn't show label) + * + * Display mode of a #PhoshAppGridButton + */ +typedef enum /*< enum,prefix=PHOSH >*/ +{ + PHOSH_APP_GRID_BUTTON_LAUNCHER, /*< nick=launcher >*/ + PHOSH_APP_GRID_BUTTON_FAVORITES, /*< nick=favorites >*/ +} PhoshAppGridButtonMode; + +G_DECLARE_DERIVABLE_TYPE (PhoshAppGridButton, phosh_app_grid_button, PHOSH, APP_GRID_BUTTON, PhoshAppGridBaseButton) + +struct _PhoshAppGridButtonClass { + PhoshAppGridBaseButtonClass parent_class; +}; + +GtkWidget *phosh_app_grid_button_new (GAppInfo *info); +GtkWidget *phosh_app_grid_button_new_favorite (GAppInfo *info); +void phosh_app_grid_button_set_app_info (PhoshAppGridButton *self, + GAppInfo *info); +GAppInfo *phosh_app_grid_button_get_app_info (PhoshAppGridButton *self); +gboolean phosh_app_grid_button_is_favorite (PhoshAppGridButton *self); +void phosh_app_grid_button_set_mode (PhoshAppGridButton *self, + PhoshAppGridButtonMode mode); +PhoshAppGridButtonMode phosh_app_grid_button_get_mode (PhoshAppGridButton *self); +void phosh_app_grid_button_set_folder_info (PhoshAppGridButton *self, + PhoshFolderInfo *folder_info); + +G_END_DECLS diff --git a/src/app-grid-folder-button.c b/src/app-grid-folder-button.c new file mode 100644 index 000000000..ad105a239 --- /dev/null +++ b/src/app-grid-folder-button.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-app-grid-folder-button" + +#include "app-grid-button.h" +#include "app-grid-folder-button.h" + +/** + * PhoshAppGridFolderButton: + * + * A widget to display the apps in a folder. + */ + +enum { + PROP_0, + PROP_FOLDER_INFO, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + FOLDER_LAUNCHED, + N_SIGNALS, +}; +static guint signals[N_SIGNALS]; + +struct _PhoshAppGridFolderButton { + PhoshAppGridBaseButton parent; + + PhoshFolderInfo *folder_info; + GtkGrid *grid; +}; + +G_DEFINE_TYPE (PhoshAppGridFolderButton, phosh_app_grid_folder_button, PHOSH_TYPE_APP_GRID_BASE_BUTTON); + + +static void +phosh_app_grid_folder_button_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridFolderButton *self = PHOSH_APP_GRID_FOLDER_BUTTON (object); + + switch (property_id) { + case PROP_FOLDER_INFO: + self->folder_info = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_app_grid_folder_button_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAppGridFolderButton *self = PHOSH_APP_GRID_FOLDER_BUTTON (object); + + switch (property_id) { + case PROP_FOLDER_INFO: + g_value_set_object (value, self->folder_info); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_activated_cb (PhoshAppGridFolderButton *self) +{ + g_signal_emit (self, signals[FOLDER_LAUNCHED], 0, self->folder_info); +} + + +static void +build_2x2_grid_icon (PhoshAppGridFolderButton *self) +{ + GListModel *apps; + g_autoptr (GAppInfo) app_info = NULL; + GIcon *icon; + + gtk_grid_remove_row (self->grid, 1); + gtk_grid_remove_row (self->grid, 0); + + apps = G_LIST_MODEL (phosh_folder_info_get_app_infos (self->folder_info)); + + for (int i = 0; + i < 4 && (app_info = g_list_model_get_item (apps, i)) != NULL; + i++) { + GtkWidget *image = NULL; + app_info = g_list_model_get_item (apps, i); + + icon = g_app_info_get_icon (app_info); + + if (icon == NULL) { + image = gtk_image_new_from_icon_name (PHOSH_APP_UNKNOWN_ICON, -1); + } else { + if (G_IS_THEMED_ICON (icon)) { + g_themed_icon_append_name (G_THEMED_ICON (icon), + PHOSH_APP_UNKNOWN_ICON); + } + image = gtk_image_new_from_gicon (icon, -1); + } + + /* app-grid-button uses 64px for its icon. + * Our grid has two rows and two columns, with 8px for spacing. + * So 2x + 8 = 64. + * It means x = 28. But we use 24 as icons usually have a 2px padding. */ + gtk_image_set_pixel_size (GTK_IMAGE (image), 24); + gtk_widget_set_visible (image, TRUE); + gtk_grid_attach (self->grid, image, i % 2, i >= 2, 1, 1); + } +} + + +static void +phosh_app_grid_folder_button_dispose (GObject *object) +{ + PhoshAppGridFolderButton *self = PHOSH_APP_GRID_FOLDER_BUTTON (object); + + g_clear_object (&self->folder_info); + + G_OBJECT_CLASS (phosh_app_grid_folder_button_parent_class)->dispose (object); +} + + +static void +phosh_app_grid_folder_button_constructed (GObject *object) +{ + PhoshAppGridFolderButton *self = PHOSH_APP_GRID_FOLDER_BUTTON (object); + GListModel *apps; + + G_OBJECT_CLASS (phosh_app_grid_folder_button_parent_class)->constructed (object); + + apps = G_LIST_MODEL (phosh_folder_info_get_app_infos (self->folder_info)); + + g_signal_connect_object (apps, "items-changed", G_CALLBACK (build_2x2_grid_icon), self, G_CONNECT_SWAPPED); + g_object_bind_property (self->folder_info, "name", self, "label", G_BINDING_SYNC_CREATE); + build_2x2_grid_icon (self); +} + + +static void +phosh_app_grid_folder_button_class_init (PhoshAppGridFolderButtonClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_app_grid_folder_button_set_property; + object_class->get_property = phosh_app_grid_folder_button_get_property; + object_class->dispose = phosh_app_grid_folder_button_dispose; + object_class->constructed = phosh_app_grid_folder_button_constructed; + + /** + * PhoshAppGridFolderButton:folder-info: + * + * The folder info whose apps the widget displays + */ + props[PROP_FOLDER_INFO] = + g_param_spec_object ("folder-info", "", "", + PHOSH_TYPE_FOLDER_INFO, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[FOLDER_LAUNCHED] = g_signal_new ("folder-launched", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, PHOSH_TYPE_FOLDER_INFO); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/app-grid-folder-button.ui"); + + gtk_widget_class_bind_template_callback (widget_class, on_activated_cb); + gtk_widget_class_bind_template_child (widget_class, PhoshAppGridFolderButton, grid); + + gtk_widget_class_set_css_name (widget_class, "phosh-app-grid-folder-button"); +} + + +static void +phosh_app_grid_folder_button_init (PhoshAppGridFolderButton *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_app_grid_folder_button_new_from_folder_info (PhoshFolderInfo *folder_info) +{ + return g_object_new (PHOSH_TYPE_APP_GRID_FOLDER_BUTTON, "folder-info", folder_info, NULL); +} diff --git a/src/app-grid-folder-button.h b/src/app-grid-folder-button.h new file mode 100644 index 000000000..45de80a76 --- /dev/null +++ b/src/app-grid-folder-button.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "app-grid-base-button.h" +#include "folder-info.h" + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_APP_GRID_FOLDER_BUTTON phosh_app_grid_folder_button_get_type () +G_DECLARE_FINAL_TYPE (PhoshAppGridFolderButton, phosh_app_grid_folder_button, PHOSH, APP_GRID_FOLDER_BUTTON, PhoshAppGridBaseButton) + +GtkWidget *phosh_app_grid_folder_button_new_from_folder_info (PhoshFolderInfo *folder_info); + +G_END_DECLS diff --git a/src/app-grid.c b/src/app-grid.c new file mode 100644 index 000000000..cc2fe6fbd --- /dev/null +++ b/src/app-grid.c @@ -0,0 +1,823 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "phosh-app-grid" + +#define _GNU_SOURCE +#include + +#include "app-grid.h" +#include "app-grid-button.h" +#include "app-grid-folder-button.h" +#include "app-list-model.h" +#include "favorite-list-model.h" +#include "shell-priv.h" +#include "util.h" + +#include "gtk-list-models/gtksortlistmodel.h" +#include "gtk-list-models/gtkfilterlistmodel.h" + +#include + +#define ACTIVE_SEARCH_CLASS "search-active" + +#define SEARCH_DEBOUNCE 350 +#define DEFAULT_GTK_DEBOUNCE 150 + +enum { + PROP_0, + PROP_FILTER_ADAPTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + APP_LAUNCHED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct _PhoshAppGridPrivate PhoshAppGridPrivate; +struct _PhoshAppGridPrivate { + GtkFilterListModel *model; + + GtkWidget *deck; + GtkWidget *search; + GtkWidget *apps; + GtkWidget *favs; + GtkWidget *favs_revealer; + GtkWidget *scrolled_window; + GtkWidget *btn_adaptive; + GtkWidget *btn_adaptive_img; + GtkWidget *btn_adaptive_lbl; + GtkWidget *empty_folder_label; + GtkWidget *folder_stack; + GtkWidget *folder_name_btn; + GtkWidget *folder_name_img; + GtkWidget *folder_name_entry; + GtkWidget *folder_name_label; + GtkWidget *folder_apps; + + PhoshFolderInfo *open_folder; + int open_folder_idx; + GListModel *folder_model; + + char *search_string; + gboolean filter_adaptive; + GSettings *settings; + GStrv force_adaptive; + GSimpleActionGroup *actions; + PhoshAppFilterModeFlags filter_mode; + guint debounce; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshAppGrid, phosh_app_grid, GTK_TYPE_BOX) + +static void +phosh_app_grid_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAppGrid *self = PHOSH_APP_GRID (object); + + switch (property_id) { + case PROP_FILTER_ADAPTIVE: + phosh_app_grid_set_filter_adaptive (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_app_grid_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAppGrid *self = PHOSH_APP_GRID (object); + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + switch (property_id) { + case PROP_FILTER_ADAPTIVE: + g_value_set_boolean (value, priv->filter_adaptive); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +show_main_grid (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GtkFlowBoxChild *button = NULL; + hdy_deck_set_visible_child_name (HDY_DECK (priv->deck), "main_grid"); + + if (priv->open_folder == NULL) + return; + + g_signal_handlers_disconnect_by_data (priv->folder_model, self); + priv->folder_model = NULL; + g_clear_object (&priv->open_folder); + + if (priv->open_folder_idx == -1) + return; + + /* Focus the first valid button from current index to 0 */ + for (int idx = priv->open_folder_idx; idx >= 0; idx--) { + button = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (priv->apps), idx); + if (button != NULL) + break; + } + + gtk_widget_grab_focus (GTK_WIDGET (button)); +} + + +static void +show_folder_page (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + const char *page_name; + GtkFlowBoxChild *button = NULL; + + if (g_app_info_should_show (G_APP_INFO (priv->open_folder))) { + page_name = "folder_grid"; + button = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (priv->folder_apps), 0); + } else { + g_autofree char *label; + page_name = "empty_folder"; + label = g_strdup_printf ("%s folder is empty", phosh_folder_info_get_name (priv->open_folder)); + gtk_label_set_label (GTK_LABEL (priv->empty_folder_label), label); + } + + gtk_stack_set_visible_child_name (GTK_STACK (priv->folder_stack), page_name); + + if (button != NULL) + gtk_widget_grab_focus (GTK_WIDGET (button)); +} + + +static void +app_launched_cb (GtkWidget *widget, GAppInfo *info, PhoshAppGrid *self) +{ + g_signal_emit (self, signals[APP_LAUNCHED], 0, info); +} + + +static GtkWidget * +create_folder_app_launcher (gpointer item, gpointer self) +{ + GtkWidget *btn; + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + btn = phosh_app_grid_button_new (G_APP_INFO (item)); + phosh_app_grid_button_set_folder_info (PHOSH_APP_GRID_BUTTON (btn), priv->open_folder); + g_signal_connect (btn, "app-launched", G_CALLBACK (app_launched_cb), self); + + gtk_widget_set_visible (btn, TRUE); + + return btn; +} + + +static int +get_app_info_index (PhoshAppGrid *self, GAppInfo *app_info) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + for (int idx = 0;; idx++) { + g_autoptr (GAppInfo) info = g_list_model_get_item (G_LIST_MODEL (priv->model), idx); + + if (info == NULL) + return -1; + + if (g_app_info_equal (info, app_info)) + return idx; + } + + return -1; +} + + +static void +folder_launched_cb (GtkWidget *widget, + PhoshFolderInfo *info, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GListModel *model = phosh_folder_info_get_app_infos (info); + + hdy_deck_set_visible_child_name (HDY_DECK (priv->deck), "folder_page"); + g_object_bind_property (info, "name", priv->folder_name_label, "label", G_BINDING_SYNC_CREATE); + priv->folder_model = model; + g_set_object (&priv->open_folder, info); + priv->open_folder_idx = get_app_info_index (self, G_APP_INFO (info)); + g_signal_connect_object (model, + "items-changed", + G_CALLBACK (show_folder_page), + self, + G_CONNECT_SWAPPED); + gtk_entry_set_text (GTK_ENTRY (priv->folder_name_entry), ""); + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->folder_name_btn), FALSE); + show_folder_page (self); + + gtk_flow_box_bind_model (GTK_FLOW_BOX (priv->folder_apps), + model, + create_folder_app_launcher, + self, + NULL); +} + + +static int +sort_apps (gconstpointer a, gconstpointer b, gpointer data) +{ + const char *empty = ""; + GAppInfo *info1 = G_APP_INFO (a); + GAppInfo *info2 = G_APP_INFO (b); + + g_autofree char *s1 = g_utf8_casefold (g_app_info_get_name (info1), -1); + g_autofree char *s2 = g_utf8_casefold (g_app_info_get_name (info2), -1); + + return g_utf8_collate (s1 ?: empty, s2 ?: empty); +} + + +static void +update_filter_adaptive_button (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv; + const char *label, *icon_name; + + priv = phosh_app_grid_get_instance_private (self); + if (priv->filter_adaptive) { + label = _("Show All Apps"); + icon_name = "eye-open-negative-filled-symbolic"; + } else { + label = _("Show Only Mobile Friendly Apps"); + icon_name = "eye-not-looking-symbolic"; + } + + gtk_label_set_label (GTK_LABEL (priv->btn_adaptive_lbl), label); + gtk_image_set_from_icon_name (GTK_IMAGE (priv->btn_adaptive_img), icon_name, -1); +} + + +static void +on_filter_setting_changed (PhoshAppGrid *self, + GParamSpec *pspec, + gpointer *unused) +{ + PhoshAppGridPrivate *priv; + gboolean show; + + g_return_if_fail (PHOSH_IS_APP_GRID (self)); + + priv = phosh_app_grid_get_instance_private (self); + + g_strfreev (priv->force_adaptive); + priv->force_adaptive = g_settings_get_strv (priv->settings, "force-adaptive"); + priv->filter_mode = g_settings_get_flags (priv->settings, "app-filter-mode"); + + show = !!(priv->filter_mode & PHOSH_APP_FILTER_MODE_FLAGS_ADAPTIVE); + gtk_widget_set_visible (priv->btn_adaptive, show); + + gtk_filter_list_model_refilter (priv->model); +} + + +static gboolean +filter_adaptive (PhoshAppGrid *self, GDesktopAppInfo *info) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + g_autofree char *mobile = NULL; + const char *id; + + if (!(priv->filter_mode & PHOSH_APP_FILTER_MODE_FLAGS_ADAPTIVE)) + return TRUE; + + if (!priv->filter_adaptive) + return TRUE; + + mobile = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (info), "X-Purism-FormFactor"); + if (mobile && strcasestr (mobile, "mobile;")) + return TRUE; + + g_free (mobile); + mobile = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (info), "X-KDE-FormFactor"); + if (mobile && strcasestr (mobile, "handset;")) + return TRUE; + + id = g_app_info_get_id (G_APP_INFO (info)); + if (id && g_strv_contains ((const char * const*)priv->force_adaptive, id)) + return TRUE; + + return FALSE; +} + + +static gboolean +search_apps (gpointer item, gpointer data) +{ + PhoshAppGrid *self = data; + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GAppInfo *info = item; + const char *search = NULL; + + g_return_val_if_fail (priv != NULL, TRUE); + g_return_val_if_fail (priv->search != NULL, TRUE); + + search = priv->search_string; + + if (G_IS_DESKTOP_APP_INFO (info)) { + if (!filter_adaptive (self, G_DESKTOP_APP_INFO (info))) + return FALSE; + } + + /* filter out favorites when not searching */ + if (gm_str_is_null_or_empty (search)) { + if (PHOSH_IS_FOLDER_INFO (info)) + return phosh_folder_info_refilter (PHOSH_FOLDER_INFO (info), search); + if (phosh_favorite_list_model_app_is_favorite (NULL, info)) + return FALSE; + + return TRUE; + } + + if (PHOSH_IS_FOLDER_INFO (info)) + return phosh_folder_info_refilter (PHOSH_FOLDER_INFO (info), search); + + return phosh_util_matches_app_info (info, search); +} + + +static GtkWidget * +create_favorite_launcher (gpointer item, gpointer self) +{ + GtkWidget *btn = phosh_app_grid_button_new_favorite (G_APP_INFO (item)); + + g_signal_connect (btn, "app-launched", G_CALLBACK (app_launched_cb), self); + + gtk_widget_set_visible (btn, TRUE); + + return btn; +} + +static void +toggle_favorites_revealer (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + PhoshFavoriteListModel *favorites = phosh_favorite_list_model_get_default (); + gboolean criteria; + + /* Hide favorites when there are none or a search is in progress */ + criteria = ((g_list_model_get_n_items (G_LIST_MODEL (favorites)) == 0) || + !gm_str_is_null_or_empty (priv->search_string)); + + gtk_revealer_set_reveal_child (GTK_REVEALER (priv->favs_revealer), !criteria); +} + + +static void +favorites_changed (GListModel *list, guint pos, guint removed, guint added, PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + toggle_favorites_revealer (self); + + /* We don't show favorites in the main list, filter them out */ + gtk_filter_list_model_refilter (priv->model); +} + + +static GtkWidget * +create_launcher (gpointer item, gpointer self) +{ + GtkWidget *btn; + + if (PHOSH_IS_FOLDER_INFO (item)) { + btn = phosh_app_grid_folder_button_new_from_folder_info (item); + g_signal_connect (btn, "folder-launched", G_CALLBACK (folder_launched_cb), self); + } else { + btn = phosh_app_grid_button_new (G_APP_INFO (item)); + g_signal_connect (btn, "app-launched", G_CALLBACK (app_launched_cb), self); + } + + gtk_widget_set_visible (btn, TRUE); + + return btn; +} + + +static void +phosh_app_grid_init (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GtkSortListModel *sorted; + PhoshFavoriteListModel *favorites; + g_autoptr (GAction) action = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + favorites = phosh_favorite_list_model_get_default (); + + gtk_flow_box_bind_model (GTK_FLOW_BOX (priv->favs), + G_LIST_MODEL (favorites), + create_favorite_launcher, + self, + NULL); + g_signal_connect (favorites, + "items-changed", + G_CALLBACK (favorites_changed), + self); + + /* fill the grid with apps */ + sorted = gtk_sort_list_model_new (G_LIST_MODEL (phosh_app_list_model_get_default ()), + sort_apps, + NULL, + NULL); + priv->model = gtk_filter_list_model_new (G_LIST_MODEL (sorted), + search_apps, + self, + NULL); + g_object_unref (sorted); + gtk_flow_box_bind_model (GTK_FLOW_BOX (priv->apps), + G_LIST_MODEL (priv->model), + create_launcher, + self, + NULL); + + priv->settings = g_settings_new ("sm.puri.phosh"); + g_object_connect (priv->settings, + "swapped-signal::changed::force-adaptive", on_filter_setting_changed, self, + "swapped-signal::changed::app-filter-mode", on_filter_setting_changed, self, + NULL); + on_filter_setting_changed (self, NULL, NULL); + + priv->actions = g_simple_action_group_new (); + gtk_widget_insert_action_group (GTK_WIDGET (self), "app-grid", G_ACTION_GROUP (priv->actions)); + action = G_ACTION (g_property_action_new ("filter-adaptive", self, "filter-adaptive")); + g_action_map_add_action (G_ACTION_MAP (priv->actions), action); + + toggle_favorites_revealer (self); +} + + +static void +phosh_app_grid_dispose (GObject *object) +{ + PhoshAppGrid *self = PHOSH_APP_GRID (object); + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + g_clear_object (&priv->open_folder); + g_clear_object (&priv->actions); + g_clear_object (&priv->model); + g_clear_object (&priv->settings); + g_clear_handle_id (&priv->debounce, g_source_remove); + + G_OBJECT_CLASS (phosh_app_grid_parent_class)->dispose (object); +} + + +static void +phosh_app_grid_finalize (GObject *object) +{ + PhoshAppGrid *self = PHOSH_APP_GRID (object); + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + g_clear_pointer (&priv->search_string, g_free); + g_strfreev (priv->force_adaptive); + + G_OBJECT_CLASS (phosh_app_grid_parent_class)->finalize (object); +} + + +static gboolean +phosh_app_grid_key_press_event (GtkWidget *widget, + GdkEventKey *event) +{ + PhoshAppGrid *self = PHOSH_APP_GRID (widget); + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + /* Don't search when folder is open */ + if (priv->open_folder != NULL) + return GDK_EVENT_PROPAGATE; + + return gtk_search_entry_handle_event (GTK_SEARCH_ENTRY (priv->search), + (GdkEvent *) event); +} + + +static void +do_search (gpointer data) +{ + PhoshAppGrid *self = data; + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GtkAdjustment *adjustment; + gboolean search_active = TRUE; + + if (gm_str_is_null_or_empty (priv->search_string)) { + adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolled_window)); + gtk_adjustment_set_value (adjustment, 0); + search_active = FALSE; + } + + phosh_util_toggle_style_class (GTK_WIDGET (priv->apps), ACTIVE_SEARCH_CLASS, search_active); + toggle_favorites_revealer (self); + gtk_filter_list_model_refilter (priv->model); + + priv->debounce = 0; +} + + +static void +on_folder_edit_toggled (PhoshAppGrid *self, GtkToggleButton *toggle_btn) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + gboolean active = gtk_toggle_button_get_active (toggle_btn); + + if (active) { + const char *folder_name = phosh_folder_info_get_name (priv->open_folder); + gtk_entry_set_text (GTK_ENTRY (priv->folder_name_entry), folder_name); + gtk_widget_grab_focus (priv->folder_name_entry); + gtk_image_set_from_icon_name (GTK_IMAGE (priv->folder_name_img), "checkmark-symbolic", -1); + } else { + const char *folder_name = gtk_entry_get_text (GTK_ENTRY (priv->folder_name_entry)); + if (gm_str_is_null_or_empty (folder_name)) + return; + phosh_folder_info_set_name (priv->open_folder, folder_name); + gtk_entry_set_text (GTK_ENTRY (priv->folder_name_entry), ""); + gtk_image_set_from_icon_name (GTK_IMAGE (priv->folder_name_img), "document-edit-symbolic", -1); + } +} + + +static void +on_folder_entry_activated (PhoshAppGrid *self, GtkEntry *entry) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (priv->folder_name_btn), FALSE); +} + + +static void +on_search_changed (GtkSearchEntry *entry, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + const char *search = gtk_entry_get_text (GTK_ENTRY (entry)); + + g_clear_pointer (&priv->search_string, g_free); + + g_clear_handle_id (&priv->debounce, g_source_remove); + + if (search && *search != '\0') { + priv->search_string = g_utf8_casefold (search, -1); + + /* GtkSearchEntry already adds 150ms of delay, but it's too little + * so add a bit more until searching is faster and/or non-blocking */ + priv->debounce = g_timeout_add_once (SEARCH_DEBOUNCE, do_search, self); + g_source_set_name_by_id (priv->debounce, "[phosh] debounce app grid search (search-changed)"); + } else { + /* don't add the delay when the entry got cleared */ + do_search (self); + } +} + + +static void +on_search_preedit_changed (GtkSearchEntry *entry, + const char *preedit, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + g_clear_pointer (&priv->search_string, g_free); + + if (preedit && *preedit != '\0') + priv->search_string = g_utf8_casefold (preedit, -1); + + g_clear_handle_id (&priv->debounce, g_source_remove); + + priv->debounce = g_timeout_add_once (SEARCH_DEBOUNCE + DEFAULT_GTK_DEBOUNCE, do_search, self); + g_source_set_name_by_id (priv->debounce, "[phosh] debounce app grid search (preedit-changed)"); +} + + +static void +on_search_activated (GtkSearchEntry *entry, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + GtkFlowBoxChild *child; + + if (!gtk_widget_has_focus (GTK_WIDGET (entry))) + return; + + /* Don't activate when there isn't an active search */ + if (!priv->search_string || *priv->search_string == '\0') + return; + + child = gtk_flow_box_get_child_at_index (GTK_FLOW_BOX (priv->apps), 0); + + /* No results */ + if (child == NULL) + return; + + if (G_LIKELY (PHOSH_IS_APP_GRID_BUTTON (child))) { + gtk_widget_activate (GTK_WIDGET (child)); + } else { + g_critical ("Unexpected child type, %s", + g_type_name (G_TYPE_FROM_INSTANCE (child))); + } +} + + +static gboolean +on_search_lost_focus (GtkWidget *widget, + GdkEvent *event, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + phosh_util_toggle_style_class (GTK_WIDGET (priv->apps), ACTIVE_SEARCH_CLASS, FALSE); + + return GDK_EVENT_PROPAGATE; +} + + +static gboolean +on_search_gained_focus (GtkWidget *widget, + GdkEvent *event, + PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv = phosh_app_grid_get_instance_private (self); + + if (!gm_str_is_null_or_empty (priv->search_string)) + phosh_util_toggle_style_class (GTK_WIDGET (priv->apps), ACTIVE_SEARCH_CLASS, TRUE); + + return GDK_EVENT_PROPAGATE; +} + + +static void +phosh_app_grid_class_init (PhoshAppGridClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_app_grid_dispose; + object_class->finalize = phosh_app_grid_finalize; + + object_class->set_property = phosh_app_grid_set_property; + object_class->get_property = phosh_app_grid_get_property; + + widget_class->key_press_event = phosh_app_grid_key_press_event; + + /** + * PhoshAppGrid:filter-adaptive: + * + * Whether only adaptive apps should be shown + */ + props[PROP_FILTER_ADAPTIVE] = + g_param_spec_boolean ("filter-adaptive", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[APP_LAUNCHED] = + g_signal_new ("app-launched", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_APP_INFO); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/app-grid.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, apps); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, btn_adaptive); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, btn_adaptive_img); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, btn_adaptive_lbl); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, deck); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, empty_folder_label); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, favs); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, favs_revealer); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_apps); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_name_btn); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_name_img); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_name_entry); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_name_label); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, folder_stack); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, scrolled_window); + gtk_widget_class_bind_template_child_private (widget_class, PhoshAppGrid, search); + + gtk_widget_class_bind_template_callback (widget_class, on_folder_edit_toggled); + gtk_widget_class_bind_template_callback (widget_class, on_folder_entry_activated); + gtk_widget_class_bind_template_callback (widget_class, on_search_changed); + gtk_widget_class_bind_template_callback (widget_class, on_search_preedit_changed); + gtk_widget_class_bind_template_callback (widget_class, on_search_activated); + gtk_widget_class_bind_template_callback (widget_class, on_search_gained_focus); + gtk_widget_class_bind_template_callback (widget_class, on_search_lost_focus); + gtk_widget_class_bind_template_callback (widget_class, show_main_grid); + + gtk_widget_class_set_css_name (widget_class, "phosh-app-grid"); +} + + +GtkWidget * +phosh_app_grid_new (void) +{ + return g_object_new (PHOSH_TYPE_APP_GRID, NULL); +} + + +void +phosh_app_grid_reset (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv; + GtkAdjustment *adjustment; + + g_return_if_fail (PHOSH_IS_APP_GRID (self)); + + priv = phosh_app_grid_get_instance_private (self); + + adjustment = gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (priv->scrolled_window)); + + gtk_adjustment_set_value (adjustment, 0); + gtk_entry_set_text (GTK_ENTRY (priv->search), ""); + g_clear_pointer (&priv->search_string, g_free); + + /* Do not focus last folder */ + priv->open_folder_idx = -1; + /* Set duration to 0 to avoid the simultaneous animation of grid sliding up and deck sliding + * right. If not done, it feels like the deck is spiraling diagonally up. */ + hdy_deck_set_transition_duration (HDY_DECK (priv->deck), 0); + show_main_grid (self); + hdy_deck_set_transition_duration (HDY_DECK (priv->deck), 200); +} + + +void +phosh_app_grid_focus_search (PhoshAppGrid *self) +{ + PhoshAppGridPrivate *priv; + + g_return_if_fail (PHOSH_IS_APP_GRID (self)); + priv = phosh_app_grid_get_instance_private (self); + gtk_widget_grab_focus (priv->search); +} + + +gboolean +phosh_app_grid_handle_search (PhoshAppGrid *self, GdkEvent *event) +{ + PhoshAppGridPrivate *priv; + gboolean ret; + + g_return_val_if_fail (PHOSH_IS_APP_GRID (self), GDK_EVENT_PROPAGATE); + priv = phosh_app_grid_get_instance_private (self); + + /* Prevent stealing of focus when folder is open and it's name entry is active */ + if (priv->open_folder != NULL) + return GDK_EVENT_PROPAGATE; + + ret = gtk_search_entry_handle_event (GTK_SEARCH_ENTRY (priv->search), event); + if (ret == GDK_EVENT_STOP) + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->search)); + + return ret; +} + + +void +phosh_app_grid_set_filter_adaptive (PhoshAppGrid *self, gboolean enable) +{ + PhoshAppGridPrivate *priv; + + g_debug ("Filter-adaptive: %d", enable); + + g_return_if_fail (PHOSH_IS_APP_GRID (self)); + priv = phosh_app_grid_get_instance_private (self); + + if (priv->filter_adaptive == enable) + return; + + priv->filter_adaptive = enable; + update_filter_adaptive_button (self); + + gtk_filter_list_model_refilter (priv->model); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FILTER_ADAPTIVE]); +} diff --git a/src/app-grid.h b/src/app-grid.h new file mode 100644 index 000000000..ce4daac2e --- /dev/null +++ b/src/app-grid.h @@ -0,0 +1,34 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#include + +#include "phosh-settings-enums.h" + +#pragma once + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_APP_GRID phosh_app_grid_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshAppGrid, phosh_app_grid, PHOSH, APP_GRID, GtkBox) + +struct _PhoshAppGridClass +{ + GtkBoxClass parent_class; +}; + +GtkWidget *phosh_app_grid_new (void); +void phosh_app_grid_reset (PhoshAppGrid *self); +void phosh_app_grid_focus_search (PhoshAppGrid *self); +gboolean phosh_app_grid_handle_search (PhoshAppGrid *self, GdkEvent *event); +void phosh_app_grid_set_filter_adaptive (PhoshAppGrid *self, gboolean enable); + + +G_END_DECLS diff --git a/src/app-list-model.c b/src/app-list-model.c new file mode 100644 index 000000000..d58fa8936 --- /dev/null +++ b/src/app-list-model.c @@ -0,0 +1,366 @@ +/* + * Copyright © 2019-2020 Zander Brown + * 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Inspired by gliststore.c: + * Copyright 2015 Lars Uebernickel + * Copyright 2015 Ryan Lortie + * https://gitlab.gnome.org/GNOME/glib/blob/713fec9dcb1ee49c4f64bbb6f483a5cd1db9966a/gio/gliststore.c + * + * Author: Zander Brown + */ + +#include "app-list-model.h" +#include "folder-info.h" + +#include + +#include + +typedef struct _PhoshAppListModelPrivate PhoshAppListModelPrivate; +struct _PhoshAppListModelPrivate { + GAppInfoMonitor *monitor; + + GSequence *items; + + guint debounce; + + /* cache */ + struct { + gboolean is_valid; + guint position; + GSequenceIter *iter; + } last; + + GSettings *settings; + + GHashTable *startup_wm_class; + GHashTable *exec_to_id; +}; + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshAppListModel, phosh_app_list_model, G_TYPE_OBJECT, + G_ADD_PRIVATE (PhoshAppListModel) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +static void +phosh_app_list_model_finalize (GObject *object) +{ + PhoshAppListModel *self = PHOSH_APP_LIST_MODEL (object); + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + g_clear_handle_id (&priv->debounce, g_source_remove); + + g_clear_pointer (&priv->startup_wm_class, g_hash_table_destroy); + g_clear_pointer (&priv->exec_to_id, g_hash_table_destroy); + g_clear_object (&priv->monitor); + g_clear_object (&priv->settings); + + g_sequence_free (priv->items); + + G_OBJECT_CLASS (phosh_app_list_model_parent_class)->finalize (object); +} + + +static void +phosh_app_list_model_class_init (PhoshAppListModelClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = phosh_app_list_model_finalize; +} + + +static GType +list_get_item_type (GListModel *list) +{ + return G_TYPE_APP_INFO; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshAppListModel *self = PHOSH_APP_LIST_MODEL (list); + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + GSequenceIter *it = NULL; + + if (priv->last.is_valid) + { + if (position < G_MAXUINT && priv->last.position == position + 1) + it = g_sequence_iter_prev (priv->last.iter); + else if (position > 0 && priv->last.position == position - 1) + it = g_sequence_iter_next (priv->last.iter); + else if (priv->last.position == position) + it = priv->last.iter; + } + + if (it == NULL) + it = g_sequence_get_iter_at_pos (priv->items, position); + + priv->last.iter = it; + priv->last.position = position; + priv->last.is_valid = TRUE; + + if (g_sequence_iter_is_end (it)) + return NULL; + else + return g_object_ref (g_sequence_get (it)); + +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshAppListModel *self = PHOSH_APP_LIST_MODEL (list); + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + return g_sequence_get_length (priv->items); +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static GList* +filter_out_apps_in_folder (GList *apps, PhoshFolderInfo *folder_info) +{ + GList *node, *prev; + + /* The length of the list is always at least 2. The first element is always the `folder_info`. + * This makes the logic easier as otherwise we would have to check for edge-cases like first + * element itself part of folder etc. */ + + for (node = g_list_next (apps); node; node = g_list_next (node)) { + GAppInfo *app_info = G_APP_INFO (node->data); + if (phosh_folder_info_contains (folder_info, app_info)) { + prev = g_list_previous (node); + apps = g_list_delete_link (apps, node); + node = prev; + } + } + + return apps; +} + + +static void on_folder_children_changed (PhoshAppListModel *self); + + +static void +emit_items_changed (PhoshAppListModel *self) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + int added = g_sequence_get_length (priv->items); + int removed = added; + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); +} + + +static gboolean +items_changed (gpointer data) +{ + PhoshAppListModel *self = PHOSH_APP_LIST_MODEL (data); + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + g_auto (GStrv) folder_paths = NULL; + g_autolist (GAppInfo) new_apps = NULL; + int removed; + int added = 0; + + new_apps = g_app_info_get_all (); + + g_return_val_if_fail (new_apps != NULL, G_SOURCE_REMOVE); + + removed = g_sequence_get_length (priv->items); + g_sequence_remove_range (g_sequence_get_begin_iter (priv->items), + g_sequence_get_end_iter (priv->items)); + g_hash_table_remove_all (priv->startup_wm_class); + g_hash_table_remove_all (priv->exec_to_id); + + folder_paths = g_settings_get_strv (priv->settings, "folder-children"); + + for (int i = 0; i < g_strv_length (folder_paths); i++) { + char *path = folder_paths[i]; + PhoshFolderInfo *folder_info = phosh_folder_info_new_from_folder_path (path); + new_apps = g_list_prepend (new_apps, folder_info); + new_apps = filter_out_apps_in_folder (new_apps, folder_info); + g_signal_connect_object (folder_info, "apps-changed", G_CALLBACK (on_folder_children_changed), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (folder_info, "notify::name", G_CALLBACK (emit_items_changed), + self, G_CONNECT_SWAPPED); + } + + for (GList *l = new_apps; l; l = g_list_next (l)) { + const char *startup_wm_class; + GAppInfo *app_info = l->data; + + /* We add folders irrespective of their emptiness because otherwise we won't be able to listen + * for apps-changed signal. */ + if (!PHOSH_IS_FOLDER_INFO (app_info) && !g_app_info_should_show (app_info)) + continue; + + g_sequence_append (priv->items, g_object_ref (app_info)); + added++; + + if (!G_IS_DESKTOP_APP_INFO (app_info)) + continue; + + startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (app_info)); + if (startup_wm_class) { + g_hash_table_insert (priv->startup_wm_class, + g_strdup (startup_wm_class), + g_object_ref (app_info)); + } + } + + priv->last.is_valid = FALSE; + priv->last.iter = NULL; + priv->last.position = 0; + + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); + + priv->debounce = 0; + + return G_SOURCE_REMOVE; +} + + +static void +on_monitor_changed_cb (GAppInfoMonitor *monitor, + gpointer data) +{ + PhoshAppListModel *self = PHOSH_APP_LIST_MODEL (data); + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + if (priv->debounce != 0) { + g_source_remove (priv->debounce); + } + priv->debounce = g_timeout_add (500, items_changed, data); + g_source_set_name_by_id (priv->debounce, "[phosh] debounce app changes"); +} + + +static void +on_folder_children_changed (PhoshAppListModel *self) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + /* A folder has been created or destroyed or modified. + * Rearrange the apps from scratch. */ + on_monitor_changed_cb (priv->monitor, self); +} + + +static void +phosh_app_list_model_init (PhoshAppListModel *self) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + priv->debounce = 0; + priv->startup_wm_class = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + priv->exec_to_id = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); + + priv->last.is_valid = FALSE; + + priv->items = g_sequence_new ((GDestroyNotify) g_object_unref); + priv->monitor = g_app_info_monitor_get (); + g_signal_connect (priv->monitor, "changed", G_CALLBACK (on_monitor_changed_cb), self); + + priv->settings = g_settings_new (PHOSH_FOLDERS_SCHEMA_ID); + g_signal_connect_object (priv->settings, "changed::folder-children", + G_CALLBACK (on_folder_children_changed), + self, G_CONNECT_SWAPPED); + + on_monitor_changed_cb (priv->monitor, self); +} + +/** + * phosh_app_list_model_get_default: + * + * Return Value: (transfer none): The global #PhoshAppListModel singleton + */ +PhoshAppListModel * +phosh_app_list_model_get_default (void) +{ + static PhoshAppListModel *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_APP_LIST_MODEL, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *) &instance); + } + + return instance; +} + +/** + * phosh_app_list_model_lookup_by_startup_wm_class: + * @self: The app list model + * @class: The `StartupWMClass` + * + * Lookup the app-id by the wm class of the toplevel. + * https://specifications.freedesktop.org/desktop-entry-spec/latest/recognized-keys.html + * + * Returns: (transfer full)(nullable): GDesktopAppInfo for requested app_id + */ +GDesktopAppInfo * +phosh_app_list_model_lookup_by_startup_wm_class (PhoshAppListModel *self, + const char *class) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_APP_LIST_MODEL (self), NULL); + g_return_val_if_fail (class, NULL); + + return g_hash_table_lookup (priv->startup_wm_class, class); +} + + +void +phosh_app_list_model_add_exec (PhoshAppListModel *self, + const char *exec, + GAppInfo *info) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + g_autofree char *bin = NULL, *cmd = NULL; + g_auto (GStrv) parts = NULL; + + g_return_if_fail (PHOSH_IS_APP_LIST_MODEL (self)); + g_return_if_fail (G_IS_APP_INFO (info)); + g_return_if_fail (exec); + + parts = g_strsplit (exec, " ", -1); + if (gm_strv_is_null_or_empty (parts)) + return; + + cmd = g_path_get_basename (parts[0]); + if (gm_str_is_null_or_empty (cmd)) + return; + + g_hash_table_insert (priv->exec_to_id, g_steal_pointer (&cmd), g_object_ref (info)); +} + + +GDesktopAppInfo * +phosh_app_list_model_lookup_by_exec (PhoshAppListModel *self, const char *exec) +{ + PhoshAppListModelPrivate *priv = phosh_app_list_model_get_instance_private (self); + + return g_hash_table_lookup (priv->exec_to_id, exec); +} diff --git a/src/app-list-model.h b/src/app-list-model.h new file mode 100644 index 000000000..4ad506035 --- /dev/null +++ b/src/app-list-model.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_APP_LIST_MODEL phosh_app_list_model_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshAppListModel, phosh_app_list_model, PHOSH, APP_LIST_MODEL, GObject) + +struct _PhoshAppListModelClass +{ + GObjectClass parent_class; +}; + +PhoshAppListModel *phosh_app_list_model_get_default (void); +GDesktopAppInfo * phosh_app_list_model_lookup_by_startup_wm_class (PhoshAppListModel *self, + const char *class); +GDesktopAppInfo * phosh_app_list_model_lookup_by_exec (PhoshAppListModel *self, const char *exec); +void phosh_app_list_model_add_exec (PhoshAppListModel *self, + const char *exec, + GAppInfo *info); + +G_END_DECLS diff --git a/src/app-tracker.c b/src/app-tracker.c new file mode 100644 index 000000000..a46200a5e --- /dev/null +++ b/src/app-tracker.c @@ -0,0 +1,743 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-app-tracker" + +#include "phosh-config.h" + +#include "app-list-model.h" +#include "app-tracker.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "toplevel-manager.h" +#include "phosh-marshalers.h" +#include "util.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#include +#include +#include + +#define STARTUP_TIMEOUT 5 + +/** + * PhoshAppTracker: + * + * Application state tracker + * + * Tracks the startup state of applications + */ + +enum { + APP_LAUNCH_STARTED, + APP_LAUNCHED, + APP_READY, + APP_FAILED, + APP_ACTIVATED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + + +/** + * PhoshAppStateFlags: + * + * Application state based on startup id + * + * PHOSH_APP_TRACKER_STATE_FLAG_UNKNOWN: App state unknown + * PHOSH_APP_TRACKER_STATE_FLAG_LAUNCH_STARTED: The app is about to be + * launched by us. + * PHOSH_APP_TRACKER_STATE_FLAG_LAUNCHED: app was launched by us + * Gio told us it spawned the process + * PHOSH_APP_TRACKER_STATE_FLAG_DBUS_LAUNCH: app launch seen on DBus via + * org.gtk.gio.DesktopAppInfo + * PHOSH_APP_TRACKER_STATE_FLAG_WL_LAUNCH: Startup id sent by launcher seen. + * The compositor got notified by the launcher about the launchee's startup id. + * This happens via the xdg-activation or the GTK specific gtk_shell1 protocol. + * The compositor then notifies phosh via the phosh-private wayland protocol. + * PHOSH_APP_TRACKER_STATE_FLAG_WL_STARTUP_ID: Startup id sent by launchee seen. + * The launchee is up and notified the compositor via it's startup id. + * This happens via the xdg-activation or the GTK specific gtk_shell1 protocol. + * The compositor then notifies phosh via the phosh-private wayland protocol. + */ +typedef enum { + PHOSH_APP_TRACKER_STATE_FLAG_UNKNOWN = 0, + PHOSH_APP_TRACKER_STATE_FLAG_LAUNCH_STARTED = (1 << 0), + PHOSH_APP_TRACKER_STATE_FLAG_LAUNCHED = (1 << 1), + PHOSH_APP_TRACKER_STATE_FLAG_DBUS_LAUNCH = (1 << 2), + PHOSH_APP_TRACKER_STATE_FLAG_WL_LAUNCH = (1 << 3), + PHOSH_APP_TRACKER_STATE_FLAG_WL_STARTUP_ID = (1 << 4), +} PhoshAppStateFlags; + +typedef struct { + gint64 pid; + PhoshAppStateFlags state; + + char *startup_id; /* (owned) */ + guint timeout_id; + GDesktopAppInfo *info; /* (owned) */ + PhoshAppTracker *tracker; /* (unowned) */ +} PhoshAppState; + +struct _PhoshAppTracker { + GObject parent; + + GDBusConnection *session_bus; + guint dbus_id; + guint idle_id; + struct phosh_private_startup_tracker *wl_tracker; /* PhoshPrivate wayland interface */ + GHashTable *apps; + GCancellable *cancel; +}; +G_DEFINE_TYPE (PhoshAppTracker, phosh_app_tracker, G_TYPE_OBJECT) + + +static gboolean +on_startup_timeout (gpointer data) +{ + PhoshAppState *state = data; + + g_return_val_if_fail (PHOSH_IS_APP_TRACKER (state->tracker), G_SOURCE_REMOVE); + + if (state->state & PHOSH_APP_TRACKER_STATE_FLAG_WL_STARTUP_ID) { + g_warning ("Hit timeout for '%s' with startup id: '%s' although it's up", + g_app_info_get_name (G_APP_INFO (state->info)), + state->startup_id); + goto out; + } + + if (!g_hash_table_contains (state->tracker->apps, state->startup_id)) { + g_warning ("No info for startup_id '%s' found", state->startup_id); + goto out; + } + + /* We got a "launched" signal but the compositor never reported the app as up */ + g_warning ("Startup of app '%s' with startup id: '%s' timed out", + g_app_info_get_name (G_APP_INFO (state->info)), + state->startup_id); + + g_signal_emit (state->tracker, signals[APP_FAILED], 0, state->info, state->startup_id); + g_hash_table_remove (state->tracker->apps, state->startup_id); + + out: + state->timeout_id = 0; + return G_SOURCE_REMOVE; +} + + +static PhoshAppState * +phosh_app_state_new (GDesktopAppInfo *info, + const char *startup_id, + gint64 pid, + PhoshAppStateFlags flags, + PhoshAppTracker *tracker) +{ + PhoshAppState *state = g_new0 (PhoshAppState, 1); + + state->startup_id = g_strdup (startup_id); + state->pid = pid; + state->state = flags; + state->info = g_object_ref (info); + state->tracker = tracker; + state->timeout_id = g_timeout_add_seconds (STARTUP_TIMEOUT, on_startup_timeout, state); + g_source_set_name_by_id (state->timeout_id, "[phosh] state timeout"); + + g_debug ("Pid %" G_GINT64_FORMAT ", '%s', startup-id: %s got state %d", + state->pid, + g_app_info_get_name (G_APP_INFO (info)), + state->startup_id, + state->state); + + return state; +} + + +static void +phosh_app_state_free (PhoshAppState *state) +{ + g_clear_handle_id (&state->timeout_id, g_source_remove); + g_object_unref (state->info); + g_free (state->startup_id); + + g_free (state); +} + + +static PhoshAppState* +update_app_state (PhoshAppTracker *self, + const char *startup_id, + PhoshAppStateFlags flags, + gint64 pid) + +{ + PhoshAppState *state; + + state = g_hash_table_lookup (self->apps, startup_id); + g_return_val_if_fail (state, NULL); + + /* Changing pid is not allowed */ + g_return_val_if_fail (!state->pid || (state->pid && state->pid != pid), state); + + state->pid = pid; + g_debug ("Pid %" G_GINT64_FORMAT ", startup-id: %s got state %d", + state->pid, + state->startup_id, + flags); + + state->state |= flags; + + return state; +} + + +static void +startup_tracker_handle_launched (void *data, + struct phosh_private_startup_tracker *startup_tracker, + const char *startup_id, + unsigned int protocol, + unsigned int flags) +{ + PhoshAppState *state; + PhoshAppTracker *self = PHOSH_APP_TRACKER (data); + + g_debug ("%s %s %d", __func__, startup_id, protocol); + g_return_if_fail (PHOSH_IS_APP_TRACKER (self)); + g_return_if_fail (startup_id != NULL); + + state = g_hash_table_lookup (self->apps, startup_id); + /* The compositor notified us about a startup-id we didn't know about yet */ + if (!state) { + g_warning ("No info for startup_id '%s' found", startup_id); + return; + } + + update_app_state (self, startup_id, PHOSH_APP_TRACKER_STATE_FLAG_WL_LAUNCH, 0); +} + + +static void +startup_tracker_handle_startup_id (void *data, + struct phosh_private_startup_tracker *startup_tracker, + const char *startup_id, + unsigned int protocol, + unsigned int flags) + +{ + PhoshAppState *state; + PhoshAppTracker *self = PHOSH_APP_TRACKER (data); + + g_debug ("%s %s %d", __func__, startup_id, protocol); + g_return_if_fail (PHOSH_IS_APP_TRACKER (self)); + g_return_if_fail (startup_id != NULL); + + state = g_hash_table_lookup (self->apps, startup_id); + /* Apps often reuse the the startup_id for multiple windows */ + if (!state) { + g_debug ("No info for startup_id '%s' found", startup_id); + return; + } + + update_app_state (self, startup_id, PHOSH_APP_TRACKER_STATE_FLAG_WL_STARTUP_ID, 0); + g_signal_emit (self, signals[APP_READY], 0, state->info, startup_id); + + /* Startup sequence done */ + g_hash_table_remove (self->apps, startup_id); +} + + +static const struct phosh_private_startup_tracker_listener startup_tracker_listener = { + .startup_id = startup_tracker_handle_startup_id, + .launched = startup_tracker_handle_launched, +}; + + +static void +on_app_launch_started (PhoshAppTracker *self, + GDesktopAppInfo *info, + GVariant *platform_data, + GAppLaunchContext *context) +{ + g_autofree char *startup_id = NULL; + PhoshAppState *state; + + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + /* + * We can't do anything useful if the compositor doesn't send events + * so make sure the user is aware. + */ + g_return_if_fail (self->wl_tracker); + + /* Application doesn't handle startup notifications */ + if (!g_desktop_app_info_get_boolean (info, "StartupNotify")) + return; + + /* Launched via spawn */ + g_variant_lookup (platform_data, "startup-notification-id", "s", &startup_id); + + /* No startup_id for e.g. Qt apps */ + if (!startup_id) { + g_debug ("No startup_id for %s", g_app_info_get_id (G_APP_INFO (info))); + return; + } + + g_return_if_fail (startup_id); + /* If we saw the startup-id already, something is wrong */ + g_return_if_fail (!g_hash_table_contains (self->apps, startup_id)); + + state = phosh_app_state_new (info, startup_id, 0, + PHOSH_APP_TRACKER_STATE_FLAG_LAUNCH_STARTED, + self); + g_hash_table_insert (self->apps, g_steal_pointer (&startup_id), state); + + g_debug ("Launch started for app '%s' with startup id: '%s'", + g_app_info_get_name (G_APP_INFO (info)), + state->startup_id); + + g_signal_emit (self, signals[APP_LAUNCH_STARTED], + g_quark_from_static_string ("self"), + info, + state->startup_id); +} + + +static void +on_start_systemd_scope_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + gboolean success; + g_autoptr (GError) err = NULL; + g_autofree char * app_id = user_data; + + success = gnome_start_systemd_scope_finish (res, &err); + if (!success) { + g_warning ("Failed move '%s' to transient systemd unit: %s", app_id, err->message); + return; + } + + g_debug ("Moved '%s' to transient systemd unit", app_id); +} + + +static void +on_app_launched (PhoshAppTracker *self, + GDesktopAppInfo *info, + GVariant *platform_data, + GAppLaunchContext *context) +{ + g_autofree char *startup_id = NULL; + const char *app_id; + PhoshAppState *state; + gint32 pid; + + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + + /* Launched via spawn */ + g_variant_lookup (platform_data, "startup-notification-id", "s", &startup_id); + g_variant_lookup (platform_data, "pid", "i", &pid); + app_id = g_app_info_get_id (G_APP_INFO (info)); + + /* Application doesn't handle startup notifications */ + if (!g_desktop_app_info_get_boolean (info, "StartupNotify")) + goto out; + + /* No startup_id for e.g. Qt apps */ + if (!startup_id) { + g_debug ("No startup_id for %s", app_id); + goto out; + } + + g_debug ("Launched app '%s' with startup id: '%s'", + g_app_info_get_name (G_APP_INFO (info)), + startup_id); + + state = update_app_state (self, startup_id, PHOSH_APP_TRACKER_STATE_FLAG_LAUNCHED, pid); + if (!state) + goto out; + + g_signal_emit (self, signals[APP_LAUNCHED], + g_quark_from_static_string ("self"), + info, + state->startup_id); + out: + if (pid && app_id) { + gnome_start_systemd_scope (app_id, + pid, + "Application launched by phosh", + self->session_bus, + self->cancel, + on_start_systemd_scope_done, + g_strdup (app_id)); + } + + g_object_unref (context); +} + +static void +on_app_launch_failed (PhoshAppTracker *self, + char *startup_id, + GAppLaunchContext *context) +{ + PhoshAppState *state; + + g_return_if_fail (PHOSH_IS_APP_TRACKER (self)); + g_return_if_fail (startup_id != NULL); + + state = g_hash_table_lookup (self->apps, startup_id); + if (!state) { + g_debug ("No info for startup_id '%s' found", startup_id); + goto out; + } + + g_warning ("Failed to launch app '%s' with startup id: '%s'", + g_app_info_get_name (G_APP_INFO (state->info)), + state->startup_id); + + g_signal_emit (self, signals[APP_FAILED], 0, state->info, startup_id); + + g_hash_table_remove (self->apps, startup_id); + out: + g_object_unref (context); +} + + +static void +on_dbus_app_launched (GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer data) +{ + gint64 pid; + PhoshAppTracker *self = PHOSH_APP_TRACKER (data); + g_autoptr (GVariant) var_dict = NULL, var_desktop_file = NULL; + g_autofree char *startup_id = NULL; + const char *desktop_file = NULL; + GVariantDict dict; + + g_return_if_fail (PHOSH_IS_APP_TRACKER (self)); + /* + * We can't do anything useful if the compositor doesn't send events + * so make sure the user is aware. + */ + g_return_if_fail (self->wl_tracker); + + g_variant_get (parameters, "(@aysxas@a{sv})", &var_desktop_file, NULL, &pid, NULL, &var_dict); + + desktop_file = g_variant_get_bytestring (var_desktop_file); + + if (desktop_file == NULL || *desktop_file == '\0') + return; + + g_variant_dict_init (&dict, var_dict); + g_variant_dict_lookup (&dict, "startup-id", "s", &startup_id); + + if (!startup_id) + return; + + if (g_hash_table_contains (self->apps, startup_id)) { + /* App already known, likely launched by us */ + g_debug ("'%s' (%s) already known", startup_id, desktop_file); + update_app_state (self, startup_id, PHOSH_APP_TRACKER_STATE_FLAG_DBUS_LAUNCH, 0); + } else { + GDesktopAppInfo *info; + PhoshAppState *state; + g_autofree char *app_id = g_path_get_basename (desktop_file); + + info = g_desktop_app_info_new (app_id); + if (!info) { + g_debug ("No desktop file for '%s'", app_id); + return; + } + + g_debug ("DBus launch %s startup-id %s", desktop_file, startup_id); + state = phosh_app_state_new (info, startup_id, pid, + PHOSH_APP_TRACKER_STATE_FLAG_DBUS_LAUNCH, + self); + g_hash_table_insert (self->apps, g_steal_pointer (&startup_id), state); + + /* There's no "Launch-started" on DBus. We fake it here so upper layers don't + need to worry about the details */ + g_signal_emit (self, signals[APP_LAUNCH_STARTED], + g_quark_from_static_string ("gio-dbus"), + info, + state->startup_id); + + g_signal_emit (self, signals[APP_LAUNCHED], + g_quark_from_static_string ("gio-dbus"), + info, + state->startup_id); + + } +} + + +static void +on_bus_get_finished (GObject *source_object, GAsyncResult *res, gpointer data) +{ + PhoshAppTracker *self = PHOSH_APP_TRACKER (data); + g_autoptr (GError) err = NULL; + GDBusConnection *session_bus; + + session_bus = g_bus_get_finish (res, &err); + if (!session_bus) { + g_warning ("Failed to attach to session bus: %s", err->message); + return; + } + self->session_bus = session_bus; + + /* Listen for spawned apps */ + self->dbus_id = g_dbus_connection_signal_subscribe (self->session_bus, + NULL, + "org.gtk.gio.DesktopAppInfo", + "Launched", + "/org/gtk/gio/DesktopAppInfo", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_dbus_app_launched, + self, NULL); +} + + +static gboolean +on_idle (PhoshAppTracker *self) +{ + g_bus_get (G_BUS_TYPE_SESSION, self->cancel, on_bus_get_finished, self); + + self->idle_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +phosh_app_tracker_finalize (GObject *object) +{ + PhoshAppTracker *self = PHOSH_APP_TRACKER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_pointer (&self->apps, g_hash_table_destroy); + g_clear_pointer (&self->wl_tracker, phosh_private_startup_tracker_destroy); + + g_clear_handle_id (&self->idle_id, g_source_remove); + if (self->dbus_id) { + g_dbus_connection_signal_unsubscribe (self->session_bus, self->dbus_id); + self->dbus_id = 0; + } + g_clear_object (&self->session_bus); + + G_OBJECT_CLASS (phosh_app_tracker_parent_class)->finalize (object); +} + + +static void +phosh_app_tracker_class_init (PhoshAppTrackerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_app_tracker_finalize; + + /** + * PhoshAppTracker::app-launch-started: + * @self: The app-tracker instance + * @app_info: The `GAppInfo` of the launched application + * @app_id: The startup-id of the launched application + * + * The app is about to be launched by the shell or external process. This + * is guaranteed to be followed by at least one PhoshAppTracker:app-launched or + * PhoshAppTracker:app-failed signal. + */ + signals[APP_LAUNCH_STARTED] = g_signal_new ("app-launch-started", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, NULL, NULL, + _phosh_marshal_VOID__OBJECT_STRING, + G_TYPE_NONE, + 2, + G_TYPE_APP_INFO, + G_TYPE_STRING); + g_signal_set_va_marshaller (signals[APP_LAUNCH_STARTED], + G_TYPE_FROM_CLASS (klass), + _phosh_marshal_VOID__OBJECT_STRINGv); + + + /** + * PhoshAppTracker::app-launched: + * @self: The app-tracker instance + * @app_info: The `GAppInfo` of the launched application + * @app_id: The startup-id of the launched application + * + * The app got spawned or DBus activated. This is guaranteed to be followed + * by a PhoshAppTracker:app-ready or PhoshAppTracker:app-failed signal. + */ + signals[APP_LAUNCHED] = g_signal_new ("app-launched", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, NULL, NULL, + _phosh_marshal_VOID__OBJECT_STRING, + G_TYPE_NONE, + 2, + G_TYPE_APP_INFO, + G_TYPE_STRING); + g_signal_set_va_marshaller (signals[APP_LAUNCHED], + G_TYPE_FROM_CLASS (klass), + _phosh_marshal_VOID__OBJECT_STRINGv); + /** + * PhoshAppTracker::app-ready: + * @self: The app-tracker instance + * @app_info: The `GAppInfo` of the ready application + * @app_id: The startup-id of the ready application + * + * The app is ready to be used by the user + */ + signals[APP_READY] = g_signal_new ("app-ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + _phosh_marshal_VOID__OBJECT_STRING, + G_TYPE_NONE, + 2, + G_TYPE_APP_INFO, + G_TYPE_STRING); + g_signal_set_va_marshaller (signals[APP_READY], + G_TYPE_FROM_CLASS (klass), + _phosh_marshal_VOID__OBJECT_STRINGv); + /** + * PhoshAppTracker::app-failed: + * @self: The app-tracker instance + * @app_info: The `GAppInfo` of the application that failed to launch + * @app_id: The startup-id of the application that failed to launch + * + * The app failed to launch. + */ + signals[APP_FAILED] = g_signal_new ("app-failed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + _phosh_marshal_VOID__OBJECT_STRING, + G_TYPE_NONE, + 2, + G_TYPE_APP_INFO, + G_TYPE_STRING); + g_signal_set_va_marshaller (signals[APP_FAILED], + G_TYPE_FROM_CLASS (klass), + _phosh_marshal_VOID__OBJECT_STRINGv); + /** + * PhoshAppTracker::app-activated: + * @self: The app-tracker instance + * @app_info: The `GAppInfo` of the activated application + * + * An already running app was activated. + */ + signals[APP_ACTIVATED] = g_signal_new ("app-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_APP_INFO); +} + + +static void +phosh_app_tracker_init (PhoshAppTracker *self) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + struct phosh_private *phosh_private = phosh_wayland_get_phosh_private (wl); + uint32_t version; + + self->cancel = g_cancellable_new (); + self->apps = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) phosh_app_state_free); + self->idle_id = g_idle_add ((GSourceFunc)on_idle, self); + g_source_set_name_by_id (self->idle_id, "[PhoshAppTracker] idle"); + + version = phosh_wayland_get_phosh_private_version (wl); + if (!phosh_private || version < PHOSH_PRIVATE_GET_STARTUP_TRACKER_SINCE_VERSION) { + g_warning ("Compositor lacks app startup tracker support"); + return; + } + + if ((self->wl_tracker = phosh_private_get_startup_tracker (phosh_private)) == NULL) { + g_critical ("Failed to retrieve startup tracker from wayland interface"); + return; + } + + phosh_private_startup_tracker_add_listener (self->wl_tracker, &startup_tracker_listener, self); +} + + +PhoshAppTracker * +phosh_app_tracker_new (void) +{ + return PHOSH_APP_TRACKER (g_object_new (PHOSH_TYPE_APP_TRACKER, NULL)); +} + +void +phosh_app_tracker_launch_app_info (PhoshAppTracker *self, GAppInfo *info) +{ + g_autoptr (GdkAppLaunchContext) context = NULL; + g_autoptr (GError) error = NULL; + PhoshShell *shell = phosh_shell_get_default (); + PhoshToplevelManager *toplevel_manager = phosh_shell_get_toplevel_manager (shell); + g_autofree char *app_id = NULL, *exec = NULL; + gboolean success; + + app_id = phosh_strip_suffix_from_app_id (g_app_info_get_id (G_APP_INFO (info))); + g_debug ("Launching '%s'", app_id); + + for (guint i = 0; i < phosh_toplevel_manager_get_num_toplevels (toplevel_manager); i++) { + PhoshToplevel *toplevel = phosh_toplevel_manager_get_toplevel (toplevel_manager, i); + const char *window_id = phosh_toplevel_get_app_id (toplevel); + g_autoptr (GDesktopAppInfo) toplevel_info = phosh_get_desktop_app_info_for_app_id (window_id); + if (toplevel_info && g_app_info_equal (G_APP_INFO (toplevel_info), G_APP_INFO (info))) { + /* activate the first matching window for now, since we don't have toplevels sorted by last-focus yet */ + phosh_toplevel_activate (toplevel, phosh_wayland_get_wl_seat (phosh_wayland_get_default ())); + g_signal_emit (self, signals[APP_ACTIVATED], 0, info); + return; + } + } + + context = gdk_display_get_app_launch_context (gdk_display_get_default ()); + g_object_ref (context); + + g_signal_connect_swapped (G_APP_LAUNCH_CONTEXT (context), + "launch-started", + G_CALLBACK (on_app_launch_started), + self); + g_signal_connect_swapped (G_APP_LAUNCH_CONTEXT (context), + "launched", + G_CALLBACK (on_app_launched), + self); + g_signal_connect_swapped (G_APP_LAUNCH_CONTEXT (context), + "launch-failed", + G_CALLBACK (on_app_launch_failed), + self); + + exec = g_desktop_app_info_get_string (G_DESKTOP_APP_INFO (info), "Exec"); + if (exec) + phosh_app_list_model_add_exec (phosh_app_list_model_get_default (), exec, info); + + success = g_desktop_app_info_launch_uris_as_manager (G_DESKTOP_APP_INFO (info), + NULL, + G_APP_LAUNCH_CONTEXT (context), + G_SPAWN_SEARCH_PATH, + NULL, NULL, + NULL, NULL, + &error); + if (!success) { + g_critical ("Failed to launch app %s: %s", + g_app_info_get_id (info), + error->message); + } +} diff --git a/src/app-tracker.h b/src/app-tracker.h new file mode 100644 index 000000000..b880c328f --- /dev/null +++ b/src/app-tracker.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_APP_TRACKER (phosh_app_tracker_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAppTracker, phosh_app_tracker, PHOSH, APP_TRACKER, GObject) + +PhoshAppTracker *phosh_app_tracker_new (void); +void phosh_app_tracker_launch_app_info (PhoshAppTracker *self, + GAppInfo *info); + +G_END_DECLS diff --git a/src/arrow.c b/src/arrow.c new file mode 100644 index 000000000..80133df42 --- /dev/null +++ b/src/arrow.c @@ -0,0 +1,189 @@ +/* + * Copyright © 2019 Alexander Mikhaylenko + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "arrow.h" + +#include + +#define WIDTH 32 +#define HEIGHT 16 +#define LENGTH 11 + +/** + * PhoshArrow: + * + * An animated arrow + * + * An animated arrow that initially points upward and + * rotates downwards as `progress` increases. + */ + +struct _PhoshArrow +{ + GtkDrawingArea parent_instance; + + double progress; +}; + +G_DEFINE_TYPE (PhoshArrow, phosh_arrow, GTK_TYPE_DRAWING_AREA) + +enum { + PROP_0, + PROP_PROGRESS, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS]; + + +static double +interpolate_progress (double t) +{ + if (t < 1.0 / 3.0) + return sin (t * 1.5 * G_PI) / 2.0; + + if (t > 2.0 / 3.0) + return cos (t * 1.5 * G_PI) / 2.0 + 1; + + return 0.5; +} + + +/* GtkWidget */ + + +static gboolean +phosh_arrow_draw (GtkWidget *widget, + cairo_t *cr) +{ + PhoshArrow *self = PHOSH_ARROW (widget); + double progress, angle, center_x, center_y; + GtkStyleContext *context; + GtkStateFlags flags; + GdkRGBA rgba; + + progress = interpolate_progress (self->progress); + + angle = (0.5 - progress) * G_PI / 2.5; + + center_x = gtk_widget_get_allocated_width (widget) / 2.0; + center_y = (gtk_widget_get_allocated_height (widget) / 2.0 - 0.5) * (0.5 + progress); + + context = gtk_widget_get_style_context (widget); + flags = gtk_widget_get_state_flags (widget); + gtk_style_context_get_color (context, flags, &rgba); + + cairo_set_line_width (cr, 3); + cairo_set_line_cap (cr, CAIRO_LINE_CAP_ROUND); + cairo_set_source_rgba (cr, rgba.red, rgba.green, rgba.blue, rgba.alpha); + + cairo_move_to (cr, center_x, center_y); + cairo_line_to (cr, center_x + LENGTH * cos (angle), center_y + LENGTH * sin (angle)); + cairo_stroke (cr); + + cairo_move_to (cr, center_x, center_y); + cairo_line_to (cr, center_x - LENGTH * cos (angle), center_y + LENGTH * sin (angle)); + cairo_stroke (cr); + + return GDK_EVENT_PROPAGATE; +} + + +static void +phosh_arrow_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshArrow *self = PHOSH_ARROW (object); + + switch (prop_id) { + case PROP_PROGRESS: + g_value_set_double (value, phosh_arrow_get_progress (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +phosh_arrow_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshArrow *self = PHOSH_ARROW (object); + + switch (prop_id) { + case PROP_PROGRESS: + phosh_arrow_set_progress (self, g_value_get_double (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +phosh_arrow_class_init (PhoshArrowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_arrow_get_property; + object_class->set_property = phosh_arrow_set_property; + widget_class->draw = phosh_arrow_draw; + + properties [PROP_PROGRESS] = + g_param_spec_double ("progress", + "Progress", + "Progress", + 0, 1, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties(object_class, N_PROPS, properties); +} + + +static void +phosh_arrow_init (PhoshArrow *self) +{ + self->progress = 0; + g_object_set (self, + "width-request", WIDTH, + "height-request", HEIGHT, + NULL); +} + + +PhoshArrow * +phosh_arrow_new (void) +{ + return g_object_new (PHOSH_TYPE_ARROW, NULL); +} + + +double +phosh_arrow_get_progress (PhoshArrow *self) +{ + g_return_val_if_fail (PHOSH_IS_ARROW (self), 0); + + return self->progress; +} + + +void +phosh_arrow_set_progress (PhoshArrow *self, + double progress) +{ + g_return_if_fail (PHOSH_IS_ARROW (self)); + + self->progress = progress; + gtk_widget_queue_draw (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_PROGRESS]); +} diff --git a/src/arrow.h b/src/arrow.h new file mode 100644 index 000000000..fc85eba33 --- /dev/null +++ b/src/arrow.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2019 Alexander Mikhaylenko + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_ARROW (phosh_arrow_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshArrow, phosh_arrow, PHOSH, ARROW, GtkDrawingArea) + +PhoshArrow *phosh_arrow_new (void); + +double phosh_arrow_get_progress (PhoshArrow *self); +void phosh_arrow_set_progress (PhoshArrow *self, + double progress); + +G_END_DECLS diff --git a/src/audio-manager.c b/src/audio-manager.c new file mode 100644 index 000000000..c49e9af74 --- /dev/null +++ b/src/audio-manager.c @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-audio-manager" + +#include "phosh-config.h" + +#include "audio-manager.h" +#include "audio/audio-devices.h" + +#include "gvc-mixer-control.h" + +#include + +/** + * PhoshAudioManager: + * + * Manage audio related properties + */ + +struct _PhoshAudioManager { + GObject parent; + + GvcMixerControl *mixer_control; + + PhoshAudioDevices *input_devices; + PhoshAudioDevices *output_devices; +}; + +G_DEFINE_TYPE (PhoshAudioManager, phosh_audio_manager, G_TYPE_OBJECT) + + +static void +phosh_audio_manager_dispose (GObject *object) +{ + PhoshAudioManager *self = PHOSH_AUDIO_MANAGER (object); + + g_clear_object (&self->output_devices); + g_clear_object (&self->input_devices); + + G_OBJECT_CLASS (phosh_audio_manager_parent_class)->dispose (object); +} + + +static void +phosh_audio_manager_finalize (GObject *object) +{ + PhoshAudioManager *self = PHOSH_AUDIO_MANAGER (object); + + g_clear_object (&self->mixer_control); + + G_OBJECT_CLASS (phosh_audio_manager_parent_class)->finalize (object); +} + + +static void +phosh_audio_manager_class_init (PhoshAudioManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_audio_manager_dispose; + object_class->finalize = phosh_audio_manager_finalize; +} + + +static void +phosh_audio_manager_init (PhoshAudioManager *self) +{ + self->mixer_control = gvc_mixer_control_new (_("Phone Shell Volume Control")); + g_return_if_fail (self->mixer_control); + gvc_mixer_control_open (self->mixer_control); + + self->output_devices = phosh_audio_devices_new (self->mixer_control, FALSE); + self->input_devices = phosh_audio_devices_new (self->mixer_control, TRUE); +} + + +PhoshAudioManager * +phosh_audio_manager_get_default (void) +{ + static PhoshAudioManager *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_AUDIO_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *) &instance); + } + + return instance; +} + + +PhoshAudioDevices * +phosh_audio_manager_get_input_devices (PhoshAudioManager *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_MANAGER (self), NULL); + + return self->input_devices; +} + + +PhoshAudioDevices * +phosh_audio_manager_get_output_devices (PhoshAudioManager *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_MANAGER (self), NULL); + + return self->output_devices; +} + + +GvcMixerControl * +phosh_audio_manager_get_mixer_control (PhoshAudioManager *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_MANAGER (self), NULL); + + return self->mixer_control; +} + + +GvcMixerStream * +phosh_audio_manager_get_default_sink (PhoshAudioManager *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_MANAGER (self), NULL); + + return gvc_mixer_control_get_default_sink (self->mixer_control); +} + + +void +phosh_audio_manager_change_input (PhoshAudioManager *self, guint id) +{ + GvcMixerUIDevice *device; + + g_return_if_fail (PHOSH_IS_AUDIO_MANAGER (self)); + + device = gvc_mixer_control_lookup_input_id (self->mixer_control, id); + g_return_if_fail (device); + + gvc_mixer_control_change_input (self->mixer_control, device); +} + + +void +phosh_audio_manager_change_output (PhoshAudioManager *self, guint id) +{ + GvcMixerUIDevice *device; + + g_return_if_fail (PHOSH_IS_AUDIO_MANAGER (self)); + + device = gvc_mixer_control_lookup_output_id (self->mixer_control, id); + g_return_if_fail (device); + + gvc_mixer_control_change_output (self->mixer_control, device); +} diff --git a/src/audio-manager.h b/src/audio-manager.h new file mode 100644 index 000000000..b59ccd34e --- /dev/null +++ b/src/audio-manager.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "audio/audio-devices.h" + +#include "gvc-mixer-control.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUDIO_MANAGER (phosh_audio_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAudioManager, phosh_audio_manager, PHOSH, AUDIO_MANAGER, GObject) + +PhoshAudioManager *phosh_audio_manager_get_default (void); +PhoshAudioDevices *phosh_audio_manager_get_input_devices (PhoshAudioManager *self); +PhoshAudioDevices *phosh_audio_manager_get_output_devices (PhoshAudioManager *self); +GvcMixerControl * phosh_audio_manager_get_mixer_control (PhoshAudioManager *self); +GvcMixerStream * phosh_audio_manager_get_default_sink (PhoshAudioManager *self); +void phosh_audio_manager_change_input (PhoshAudioManager *self, guint id); +void phosh_audio_manager_change_output (PhoshAudioManager *self, guint id); + +G_END_DECLS diff --git a/src/audio/audio-device.c b/src/audio/audio-device.c new file mode 100644 index 000000000..8a07b2a55 --- /dev/null +++ b/src/audio/audio-device.c @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-audio-device" + +#include "phosh-config.h" + +#include "audio-device.h" + +/** + * PhoshAudioDevice: + * + * Audio device information stored in [class@AudioDevices]. + */ + +enum { + PROP_0, + PROP_ID, + PROP_ICON_NAME, + PROP_DESCRIPTION, + PROP_ACTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshAudioDevice { + GObject parent; + + guint id; + char *icon_name; + char *description; + gboolean active; +}; +G_DEFINE_TYPE (PhoshAudioDevice, phosh_audio_device, G_TYPE_OBJECT) + + +static void +phosh_audio_device_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDevice *self = PHOSH_AUDIO_DEVICE (object); + + switch (property_id) { + case PROP_ID: + self->id = g_value_get_uint (value); + break; + case PROP_ICON_NAME: + self->icon_name = g_value_dup_string (value); + break; + case PROP_DESCRIPTION: + self->description = g_value_dup_string (value); + break; + case PROP_ACTIVE: + phosh_audio_device_set_active (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_device_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDevice *self = PHOSH_AUDIO_DEVICE (object); + + switch (property_id) { + case PROP_ID: + g_value_set_uint (value, self->id); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->description); + break; + case PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_device_finalize (GObject *object) +{ + PhoshAudioDevice *self = PHOSH_AUDIO_DEVICE (object); + + g_clear_pointer (&self->icon_name, g_free); + g_clear_pointer (&self->description, g_free); + + G_OBJECT_CLASS (phosh_audio_device_parent_class)->finalize (object); +} + + +static void +phosh_audio_device_class_init (PhoshAudioDeviceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_audio_device_get_property; + object_class->set_property = phosh_audio_device_set_property; + object_class->finalize = phosh_audio_device_finalize; + + props[PROP_ID] = + g_param_spec_uint ("id", "", "", + 0, G_MAXUINT, G_MAXUINT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_DESCRIPTION] = + g_param_spec_string ("description", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", "", "", + FALSE, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_audio_device_init (PhoshAudioDevice *self) +{ +} + + +PhoshAudioDevice * +phosh_audio_device_new (guint id, const char *icon_name, const char *description) +{ + return g_object_new (PHOSH_TYPE_AUDIO_DEVICE, + "id", id, + "icon-name", icon_name, + "description", description, + NULL); +} + + +const char * +phosh_audio_device_get_description (PhoshAudioDevice *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_DEVICE (self), NULL); + + return self->description; +} + + +guint +phosh_audio_device_get_id (PhoshAudioDevice *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_DEVICE (self), 0); + + return self->id; +} + +void +phosh_audio_device_set_active (PhoshAudioDevice *self, gboolean active) +{ + g_return_if_fail (PHOSH_IS_AUDIO_DEVICE (self)); + + if (self->active == active) + return; + + self->active = active; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE]); +} diff --git a/src/audio/audio-device.h b/src/audio/audio-device.h new file mode 100644 index 000000000..80740b1c2 --- /dev/null +++ b/src/audio/audio-device.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUDIO_DEVICE (phosh_audio_device_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAudioDevice, phosh_audio_device, PHOSH, AUDIO_DEVICE, GObject) + +PhoshAudioDevice *phosh_audio_device_new (guint id, + const char *icon_name, + const char *description); +guint phosh_audio_device_get_id (PhoshAudioDevice *self); +const char *phosh_audio_device_get_description (PhoshAudioDevice *self); +void phosh_audio_device_set_active (PhoshAudioDevice *self, + gboolean active); + +G_END_DECLS diff --git a/src/audio/audio-devices.c b/src/audio/audio-devices.c new file mode 100644 index 000000000..48fec6471 --- /dev/null +++ b/src/audio/audio-devices.c @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-audio-devices" + +#include "phosh-config.h" + +#include "audio-device.h" +#include "audio-devices.h" +#include "util.h" + +#include "gvc-mixer-control.h" + +#include + +#include + +/** + * PhoshAudioDevices: + * + * The currently available audio devices as a list model. The model + * can hold either input or output devices. + */ + +enum { + PROP_0, + PROP_IS_INPUT, + PROP_MIXER_CONTROL, + PROP_HAS_DEVICES, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshAudioDevices { + GObject parent; + + GListStore *devices; + gboolean is_input; + gboolean has_devices; + GvcMixerControl *mixer_control; +}; + +static void phosh_list_model_iface_init (GListModelInterface *iface); +G_DEFINE_TYPE_WITH_CODE (PhoshAudioDevices, phosh_audio_devices, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, phosh_list_model_iface_init)) + + +static void +phosh_audio_devices_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (object); + + switch (property_id) { + case PROP_IS_INPUT: + self->is_input = g_value_get_boolean (value); + break; + case PROP_MIXER_CONTROL: + self->mixer_control = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_devices_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (object); + + switch (property_id) { + case PROP_IS_INPUT: + g_value_set_boolean (value, self->is_input); + break; + case PROP_MIXER_CONTROL: + g_value_set_object (value, self->mixer_control); + break; + case PROP_HAS_DEVICES: + g_value_set_boolean (value, self->has_devices); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static GType +phosh_list_model_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_AUDIO_DEVICE; +} + + +static gpointer +phosh_list_model_get_item (GListModel *list, guint position) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (list); + + return g_list_model_get_item (G_LIST_MODEL (self->devices), position); +} + + +static unsigned int +phosh_list_model_get_n_items (GListModel *list) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (list); + + return g_list_model_get_n_items (G_LIST_MODEL (self->devices)); +} + + +static void +phosh_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = phosh_list_model_get_item_type; + iface->get_item = phosh_list_model_get_item; + iface->get_n_items = phosh_list_model_get_n_items; +} + + +static void +on_device_added (PhoshAudioDevices *self, guint id) +{ + GvcMixerUIDevice *device = NULL; + GvcMixerStream *stream = NULL; + g_autofree char *description = NULL; + const char *icon_name; + const char *origin; + g_autoptr (PhoshAudioDevice) audio_device = NULL; + guint stream_id; + + g_debug ("Adding audio device %d", id); + if (self->is_input) + device = gvc_mixer_control_lookup_input_id (self->mixer_control, id); + else + device = gvc_mixer_control_lookup_output_id (self->mixer_control, id); + + if (device == NULL) { + g_debug ("No device for id %u", id); + return; + } + + stream_id = gvc_mixer_ui_device_get_stream_id (device); + stream = gvc_mixer_control_lookup_stream_id (self->mixer_control, stream_id); + if (stream) { + const char *name; + + name = gvc_mixer_stream_get_name (stream); + /* Don't add role loopbacks as switching to them is not useful */ + if (g_str_has_prefix (name, "input.loopback.sink.role.")) + return; + } + + origin = gvc_mixer_ui_device_get_origin (device); + if (gm_str_is_null_or_empty (origin)) { + description = g_strdup (gvc_mixer_ui_device_get_description (device)); + } else { + description = g_strdup_printf ("%s - %s", + gvc_mixer_ui_device_get_description (device), + origin); + } + + icon_name = gvc_mixer_ui_device_get_icon_name (device); + audio_device = phosh_audio_device_new (id, icon_name, description); + g_list_store_append (self->devices, audio_device); +} + + +static void +on_device_removed (PhoshAudioDevices *self, guint id) +{ + g_debug ("Removing audio device %d", id); + for (int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->devices)); i++) { + g_autoptr (PhoshAudioDevice) device = g_list_model_get_item (G_LIST_MODEL (self->devices), i); + + if (id == phosh_audio_device_get_id (device)) { + g_list_store_remove (self->devices, i); + return; + } + } + g_debug ("Device %u not present, can't remove", id); +} + + +static void +on_active_udpated (PhoshAudioDevices *self, guint id) +{ + for (int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->devices)); i++) { + g_autoptr (PhoshAudioDevice) device = g_list_model_get_item (G_LIST_MODEL (self->devices), i); + gboolean active = id == phosh_audio_device_get_id (device); + + phosh_audio_device_set_active (device, active); + } +} + + +static void +phosh_audio_devices_constructed (GObject *object) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (object); + + /* Listen for new devices now that we have a mixer and know whether we handle input or output */ + g_assert (GVC_IS_MIXER_CONTROL (self->mixer_control)); + + if (self->is_input) { + g_object_connect (self->mixer_control, + "swapped-object-signal::input-added", on_device_added, self, + "swapped-object-signal::input-removed", on_device_removed, self, + "swapped-object-signal::active-input-update", on_active_udpated, self, + NULL); + } else { + g_object_connect (self->mixer_control, + "swapped-object-signal::output-added", on_device_added, self, + "swapped-object-signal::output-removed", on_device_removed, self, + "swapped-object-signal::active-output-update", on_active_udpated, self, + NULL); + } + + G_OBJECT_CLASS (phosh_audio_devices_parent_class)->constructed (object); +} + + +static void +phosh_audio_devices_dispose (GObject *object) +{ + PhoshAudioDevices *self = PHOSH_AUDIO_DEVICES (object); + + if (self->mixer_control) + g_signal_handlers_disconnect_by_data (self->mixer_control, self); + g_clear_object (&self->mixer_control); + + G_OBJECT_CLASS (phosh_audio_devices_parent_class)->dispose (object); +} + + +static void +phosh_audio_devices_class_init (PhoshAudioDevicesClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_audio_devices_get_property; + object_class->set_property = phosh_audio_devices_set_property; + object_class->constructed = phosh_audio_devices_constructed; + object_class->dispose = phosh_audio_devices_dispose; + + /** + * PhoshAudioDevices:is-input: + * + * %TRUE Whether this list model stores input devices, %FALSE for output + * devices. + */ + props[PROP_IS_INPUT] = + g_param_spec_boolean ("is-input", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshAudioDevices:mixer-control: + * + * %TRUE Whether this list model stores input devices, %FALSE for output + * devices. + */ + props[PROP_MIXER_CONTROL] = + g_param_spec_object ("mixer-control", "", "", + GVC_TYPE_MIXER_CONTROL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshAudioDevices:has-devices: + * + * %TRUE when there's at least on audio device present + */ + props[PROP_HAS_DEVICES] = + g_param_spec_boolean ("has-devices", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +on_items_changed (PhoshAudioDevices *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + gboolean has_devices; + g_autoptr (PhoshAudioDevice) device = NULL; + + g_return_if_fail (PHOSH_IS_AUDIO_DEVICES (self)); + + device = g_list_model_get_item (list, 0); + has_devices = !!device; + + if (self->has_devices != has_devices) { + self->has_devices = has_devices; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_DEVICES]); + } + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + + +static void +phosh_audio_devices_init (PhoshAudioDevices *self) +{ + self->devices = g_list_store_new (PHOSH_TYPE_AUDIO_DEVICE); + + g_signal_connect_swapped (self->devices, "items-changed", G_CALLBACK (on_items_changed), self); +} + +/** + * phosh_audio_devices_new: + * @mixer_control: A new GvcMixerControl + * @is_input: Whether this is this an input + * + * Gets a new audio devices object which exposes the currently known + * input or output devices as a list model. + */ +PhoshAudioDevices * +phosh_audio_devices_new (GvcMixerControl *mixer_control, gboolean is_input) +{ + return g_object_new (PHOSH_TYPE_AUDIO_DEVICES, + "mixer-control", mixer_control, + "is-input", is_input, + NULL); +} diff --git a/src/audio/audio-devices.h b/src/audio/audio-devices.h new file mode 100644 index 000000000..8427f72e2 --- /dev/null +++ b/src/audio/audio-devices.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "gvc-mixer-control.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUDIO_DEVICES (phosh_audio_devices_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAudioDevices, phosh_audio_devices, PHOSH, AUDIO_DEVICES, GObject) + +PhoshAudioDevices *phosh_audio_devices_new (GvcMixerControl *mixer_control, + gboolean is_input); + +G_END_DECLS diff --git a/src/auth-prompt-option.c b/src/auth-prompt-option.c new file mode 100644 index 000000000..084d75ec2 --- /dev/null +++ b/src/auth-prompt-option.c @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#include "auth-prompt-option.h" + +enum { + PROP_0, + PROP_ID, + PROP_LABEL, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +struct _PhoshAuthPromptOption { + GObject parent; + + char *id; + char *label; +}; + +G_DEFINE_TYPE (PhoshAuthPromptOption, phosh_auth_prompt_option, G_TYPE_OBJECT) + +static void +phosh_auth_prompt_option_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAuthPromptOption *obj = (PhoshAuthPromptOption *)object; + + switch (property_id) { + case PROP_ID: + g_free (obj->id); + obj->id = g_value_dup_string (value); + break; + case PROP_LABEL: + g_free (obj->label); + obj->label = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_auth_prompt_option_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAuthPromptOption *obj = (PhoshAuthPromptOption *)object; + + switch (property_id) { + case PROP_ID: + g_value_set_string (value, obj->id); + break; + case PROP_LABEL: + g_value_set_string (value, obj->label); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_auth_prompt_option_finalize (GObject *obj) +{ + PhoshAuthPromptOption *self = PHOSH_AUTH_PROMPT_OPTION (obj); + + g_free (self->label); + g_free (self->id); + + G_OBJECT_CLASS (phosh_auth_prompt_option_parent_class)->finalize (obj); +} + +static void +phosh_auth_prompt_option_class_init (PhoshAuthPromptOptionClass *class) +{ + GObjectClass *object_class = G_OBJECT_CLASS (class); + + object_class->get_property = phosh_auth_prompt_option_get_property; + object_class->set_property = phosh_auth_prompt_option_set_property; + object_class->finalize = phosh_auth_prompt_option_finalize; + + /** + * PhoshAuthPromptOption:id + * + * The internal identifier of this PhoshAuthPromptOption. + */ + props[PROP_ID] = g_param_spec_string ("id", "", "", + NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshAuthPromptOption:label + * + * The visible label of this PhoshAuthPromptOption. + */ + props[PROP_LABEL] = g_param_spec_string ("label", "", "", + NULL, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +static void +phosh_auth_prompt_option_init (PhoshAuthPromptOption *obj) +{ +} + +const char * +phosh_auth_prompt_option_get_id (PhoshAuthPromptOption *self) +{ + return self->id; +} + +const char * +phosh_auth_prompt_option_get_label (PhoshAuthPromptOption *self) +{ + return self->label; +} diff --git a/src/auth-prompt-option.h b/src/auth-prompt-option.h new file mode 100644 index 000000000..262474f9e --- /dev/null +++ b/src/auth-prompt-option.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUTH_PROMPT_OPTION (phosh_auth_prompt_option_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAuthPromptOption, phosh_auth_prompt_option, PHOSH, AUTH_PROMPT_OPTION, GObject) + +const char *phosh_auth_prompt_option_get_id (PhoshAuthPromptOption *self); +const char *phosh_auth_prompt_option_get_label (PhoshAuthPromptOption *self); + +G_END_DECLS diff --git a/src/auth.c b/src/auth.c new file mode 100644 index 000000000..442360816 --- /dev/null +++ b/src/auth.c @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-auth" + +#include "phosh-config.h" +#include "auth.h" + +#include + +/** + * PhoshAuth: + * + * PAM authentication handling + */ + +typedef struct _PhoshAuth { + GObject parent; + + pam_handle_t *pamh; +} PhoshAuth; + + +G_DEFINE_TYPE (PhoshAuth, phosh_auth, G_TYPE_OBJECT) + + +static int +pam_conversation_cb (int num_msg, + const struct pam_message **msg, + struct pam_response **resp, + void *appdata_ptr) +{ + const char *authtok = appdata_ptr; + int ret = PAM_CONV_ERR; + g_autofree struct pam_response *pam_resp = g_new0 (struct pam_response, num_msg); + + if (pam_resp == NULL) + return PAM_BUF_ERR; + + for (int i = 0; i < num_msg; ++i) { + switch (msg[i]->msg_style) { + case PAM_PROMPT_ECHO_OFF: + case PAM_PROMPT_ECHO_ON: + pam_resp[i].resp = g_strdup (authtok); + ret = PAM_SUCCESS; + break; + case PAM_ERROR_MSG: /* TBD */ + case PAM_TEXT_INFO: /* TBD */ + default: + break; + } + } + + if (ret == PAM_SUCCESS) + *resp = g_steal_pointer (&pam_resp); + + return ret; +} + + +/* return TRUE if auth token is correct, FALSE otherwise */ +static gboolean +authenticate (PhoshAuth *self, const char *authtok) +{ + int ret; + gboolean authenticated = FALSE; + const char *username; + const struct pam_conv conv = { + .conv = pam_conversation_cb, + .appdata_ptr = (void*)authtok, + }; + + if (self->pamh == NULL) { + username = g_get_user_name (); + ret = pam_start ("phosh", username, &conv, &self->pamh); + if (ret != PAM_SUCCESS) { + g_warning ("PAM start error %s", pam_strerror (self->pamh, ret)); + goto out; + } + } + + ret = pam_authenticate (self->pamh, 0); + if (ret != PAM_SUCCESS) { + if (ret != PAM_AUTH_ERR) + g_warning ("pam_authenticate error %s", pam_strerror (self->pamh, ret)); + goto out; + } + + ret = pam_acct_mgmt (self->pamh, 0); + if (ret != PAM_SUCCESS) { + g_warning ("pam_acct check failed: %s\n", pam_strerror (self->pamh, ret)); + goto out; + } + + authenticated = TRUE; + + ret = pam_end (self->pamh, ret); + if (ret != PAM_SUCCESS) + g_warning ("pam_end error %d", ret); + self->pamh = NULL; + + out: + return authenticated; +} + + +static void +authenticate_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + PhoshAuth *self = PHOSH_AUTH (source_object); + char *authtok = task_data; + gboolean ret; + + if (task_data == NULL) { + g_task_return_boolean (task, FALSE); + return; + } + + ret = authenticate (self, authtok); + g_task_return_boolean (task, ret); +} + + +static void +phosh_auth_finalize (GObject *object) +{ + PhoshAuth *self = PHOSH_AUTH (object); + GObjectClass *parent_class = G_OBJECT_CLASS (phosh_auth_parent_class); + int ret; + + if (self->pamh) { + ret = pam_end (self->pamh, PAM_AUTH_ERR); + if (ret != PAM_SUCCESS) + g_warning ("pam_end error %s", pam_strerror (self->pamh, ret)); + self->pamh = NULL; + } + + parent_class->finalize (object); +} + + +static void +phosh_auth_class_init (PhoshAuthClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_auth_finalize; +} + + +static void +phosh_auth_init (PhoshAuth *self) +{ +} + + +GObject * +phosh_auth_new (void) +{ + return g_object_new (PHOSH_TYPE_AUTH, NULL); +} + + +void +phosh_auth_authenticate_async (PhoshAuth *self, + const char *authtok, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + g_autoptr (GTask) task = NULL; + + task = g_task_new (self, cancellable, callback, callback_data); + g_task_set_task_data (task, g_strdup (authtok), g_free); + + g_task_run_in_thread (task, authenticate_thread); +} + + +gboolean +phosh_auth_authenticate_finish (PhoshAuth *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + return g_task_propagate_boolean (G_TASK (result), error); +} diff --git a/src/auth.h b/src/auth.h new file mode 100644 index 000000000..b6c5cb875 --- /dev/null +++ b/src/auth.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include +#include + +#define PHOSH_TYPE_AUTH (phosh_auth_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshAuth, phosh_auth, PHOSH, AUTH, GObject) + +GObject *phosh_auth_new (void); + +void phosh_auth_authenticate_async (PhoshAuth *self, + const char *number, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean phosh_auth_authenticate_finish (PhoshAuth *self, + GAsyncResult *result, + GError **error); + diff --git a/src/auto-brightness-bucket.c b/src/auto-brightness-bucket.c new file mode 100644 index 000000000..722fcf2a8 --- /dev/null +++ b/src/auto-brightness-bucket.c @@ -0,0 +1,217 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-auto-brightness-bucket" + +#include "phosh-config.h" + +#include "auto-brightness-bucket.h" + +/** + * PhoshAutoBrightnessBucket: + * + * Auto brightness handling using a bucket approach + */ + +enum { + PROP_0, + PROP_BRIGHTNESS, + PROP_BACKLIGHT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +typedef struct { + uint min, max; + double brightness; +} Bucket; + + +/* See https://learn.microsoft.com/en-us/windows-hardware/design/device-experiences/sensors-adaptive-brightness */ +static Bucket buckets[] = { + { 0, 10, 0.10 }, + { 5, 50, 0.25 }, + { 15, 100, 0.40 }, + { 60, 300, 0.55 }, + { 150, 400, 0.70 }, + { 250, 650, 0.85 }, + { 350, 2000, 1.00 }, + { 1000, 7000, 1.15 }, + { 5000, 10000, 1.30 }, +}; + + +struct _PhoshAutoBrightnessBucket { + GObject parent; + + PhoshBacklight *backlight; + double brightness; + uint index; +}; + + +static void auto_brightness_interface_init (PhoshAutoBrightnessInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshAutoBrightnessBucket, phosh_auto_brightness_bucket, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (PHOSH_TYPE_AUTO_BRIGHTNESS, + auto_brightness_interface_init)) + +static gboolean +is_in_bucket (uint index, double level) +{ + Bucket bucket = buckets[index]; + + if (level >= bucket.min && level <= bucket.max) + return TRUE; + + return FALSE; +} + + +static void +auto_brightness_bucket_add_ambient_level (PhoshAutoBrightness *auto_brightness, double level) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (auto_brightness); + uint index; + + if (is_in_bucket (self->index, level)) + return; + + index = self->index; + if (level < buckets[index].min) { + for (; index > 0; index--) { + if (is_in_bucket (index, level)) + break; + } + } else { + for (; index < G_N_ELEMENTS (buckets) - 1; index++) { + if (is_in_bucket (index, level)) + break; + } + } + + self->brightness = buckets[index].brightness; + + if (self->index == index) + return; + + self->index = index; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BRIGHTNESS]); +} + + +static double +auto_brightness_bucket_get_brightness (PhoshAutoBrightness *auto_brightness) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (auto_brightness); + + return self->brightness; +} + + +static PhoshBacklight * +auto_brightness_bucket_get_backlight (PhoshAutoBrightness *auto_brightness) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (auto_brightness); + + return self->backlight; +} + + +static void +auto_brightness_interface_init (PhoshAutoBrightnessInterface *iface) +{ + iface->add_ambient_level = auto_brightness_bucket_add_ambient_level; + iface->get_brightness = auto_brightness_bucket_get_brightness; + iface->get_backlight = auto_brightness_bucket_get_backlight; +} + + +static void +phosh_auto_brightness_bucket_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (object); + + switch (property_id) { + case PROP_BACKLIGHT: + g_set_object (&self->backlight, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_auto_brightness_bucket_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (object); + + switch (property_id) { + case PROP_BACKLIGHT: + g_value_set_object (value, self->backlight); + break; + case PROP_BRIGHTNESS: + g_value_set_double (value, self->brightness); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_auto_brightness_bucket_dispose (GObject *object) +{ + PhoshAutoBrightnessBucket *self = PHOSH_AUTO_BRIGHTNESS_BUCKET (object); + + g_clear_object (&self->backlight); + + G_OBJECT_CLASS (phosh_auto_brightness_bucket_parent_class)->dispose (object); +} + + +static void +phosh_auto_brightness_bucket_class_init (PhoshAutoBrightnessBucketClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_auto_brightness_bucket_get_property; + object_class->set_property = phosh_auto_brightness_bucket_set_property; + object_class->dispose = phosh_auto_brightness_bucket_dispose; + + g_object_class_override_property (object_class, PROP_BACKLIGHT, "backlight"); + props[PROP_BACKLIGHT] = g_object_class_find_property (object_class, "backlight"); + + g_object_class_override_property (object_class, PROP_BRIGHTNESS, "brightness"); + props[PROP_BRIGHTNESS] = g_object_class_find_property (object_class, "brightness"); +} + + +static void +phosh_auto_brightness_bucket_init (PhoshAutoBrightnessBucket *self) +{ + self->brightness = 0.55; + self->index = 3; +} + + +PhoshAutoBrightnessBucket * +phosh_auto_brightness_bucket_new (void) +{ + return g_object_new (PHOSH_TYPE_AUTO_BRIGHTNESS_BUCKET, NULL); +} diff --git a/src/auto-brightness-bucket.h b/src/auto-brightness-bucket.h new file mode 100644 index 000000000..f1b01de16 --- /dev/null +++ b/src/auto-brightness-bucket.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "auto-brightness.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUTO_BRIGHTNESS_BUCKET (phosh_auto_brightness_bucket_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAutoBrightnessBucket, phosh_auto_brightness_bucket, + PHOSH, AUTO_BRIGHTNESS_BUCKET, GObject) + +PhoshAutoBrightnessBucket *phosh_auto_brightness_bucket_new (void); + +G_END_DECLS diff --git a/src/auto-brightness.c b/src/auto-brightness.c new file mode 100644 index 000000000..5d84e77ea --- /dev/null +++ b/src/auto-brightness.c @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-auto-brightness" + +#include "auto-brightness.h" + +/** + * PhoshAutoBrightness: + * + * Implementations for the automatic brightness algorithm. + * + * Since: 0.51.0 + */ + +G_DEFINE_INTERFACE (PhoshAutoBrightness, phosh_auto_brightness, G_TYPE_OBJECT) + +void +phosh_auto_brightness_default_init (PhoshAutoBrightnessInterface *iface) +{ + /** + * PhoshAutoBacklight:relative-brightness: + * + * The relative brightness the given backlight should use + */ + g_object_interface_install_property ( + iface, + g_param_spec_double ("brightness", "", "", + 0.0, 1.0, 0.55, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS)); + /** + * PhoshAutoBacklight:backlight: + * + * The backlight this auto brightness handler operates on + */ + g_object_interface_install_property ( + iface, + g_param_spec_object ("backlight", "", "", + PHOSH_TYPE_BACKLIGHT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS)); +} + + +void +phosh_auto_brightness_add_ambient_level (PhoshAutoBrightness *self, double level) +{ + PhoshAutoBrightnessInterface *iface; + + g_return_if_fail (PHOSH_IS_AUTO_BRIGHTNESS (self)); + + iface = PHOSH_AUTO_BRIGHTNESS_GET_IFACE (self); + iface->add_ambient_level (self, level); +} + + +double +phosh_auto_brightness_get_brightness (PhoshAutoBrightness *self) +{ + PhoshAutoBrightnessInterface *iface; + + iface = PHOSH_AUTO_BRIGHTNESS_GET_IFACE (self); + return iface->get_brightness (self); +} + + +PhoshBacklight * +phosh_auto_brightness_get_backlight (PhoshAutoBrightness *self) +{ + PhoshAutoBrightnessInterface *iface; + + iface = PHOSH_AUTO_BRIGHTNESS_GET_IFACE (self); + return iface->get_backlight (self); +} diff --git a/src/auto-brightness.h b/src/auto-brightness.h new file mode 100644 index 000000000..eb6a9ff66 --- /dev/null +++ b/src/auto-brightness.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "backlight.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUTO_BRIGHTNESS (phosh_auto_brightness_get_type ()) +G_DECLARE_INTERFACE (PhoshAutoBrightness, phosh_auto_brightness, PHOSH, AUTO_BRIGHTNESS, GObject) + +/** + * PhoshAutobrightness: + * @parent_iface: The parent interface + * @add_ambient_level: Add a new value received by the ambient light sensor + * + * Interface implementations are required to implement all virtual functions. + */ + +struct _PhoshAutoBrightnessInterface +{ + GTypeInterface parent_iface; + + void (*add_ambient_level) (PhoshAutoBrightness *self, double value); + double (*get_brightness) (PhoshAutoBrightness *self); + PhoshBacklight *(*get_backlight) (PhoshAutoBrightness *self); +}; + +void phosh_auto_brightness_add_ambient_level (PhoshAutoBrightness *self, double level); +double phosh_auto_brightness_get_brightness (PhoshAutoBrightness *self); +PhoshBacklight *phosh_auto_brightness_get_backlight (PhoshAutoBrightness *self); diff --git a/src/background-cache.c b/src/background-cache.c new file mode 100644 index 000000000..cdd24f66e --- /dev/null +++ b/src/background-cache.c @@ -0,0 +1,214 @@ +/* + * Copyright (C) 2024-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-background-cache" + +#include "phosh-config.h" + +#include "background-cache.h" +#include "background-image.h" +#include "util.h" + +#include + +/** + * PhoshBackgroundCache: + * + * A cache of background images + */ + +struct _PhoshBackgroundCache { + GObject parent; + + GHashTable *background_images; +}; +G_DEFINE_TYPE (PhoshBackgroundCache, phosh_background_cache, G_TYPE_OBJECT) + + +static void +on_background_image_loaded (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GTask) task = G_TASK (user_data); + PhoshBackgroundCache *self; + PhoshBackgroundImage *image; + GError *err = NULL; + GFile *file; + + image = phosh_background_image_new_finish (res, &err); + if (!image) { + g_task_return_error (task, err); + return; + } + + self = PHOSH_BACKGROUND_CACHE (g_task_get_source_object (task)); + g_return_if_fail (PHOSH_IS_BACKGROUND_CACHE (self)); + file = phosh_background_image_get_file (image); + g_hash_table_insert (self->background_images, g_object_ref (file), g_object_ref (image)); + + g_task_return_pointer (task, g_steal_pointer (&image), g_object_unref); +} + + +static void +phosh_background_cache_finalize (GObject *object) +{ + PhoshBackgroundCache *self = PHOSH_BACKGROUND_CACHE (object); + + g_clear_pointer (&self->background_images, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_background_cache_parent_class)->finalize (object); +} + + +static void +phosh_background_cache_class_init (PhoshBackgroundCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_background_cache_finalize; +} + + +static void +phosh_background_cache_init (PhoshBackgroundCache *self) +{ + self->background_images = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, + g_object_unref); +} + +/** + * phosh_background_cache_get_default: + * + * Gets the background cache singleton. + * + * Returns:(transfer none): The background cache singleton. + */ +PhoshBackgroundCache * +phosh_background_cache_get_default (void) +{ + static PhoshBackgroundCache *instance; + + if (instance == NULL) { + g_debug ("Creating background cache"); + instance = g_object_new (PHOSH_TYPE_BACKGROUND_CACHE, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} + +/** + * phosh_background_cache_fetch_background: + * @self: The background cache + * @file: The file to lookup or load + * @cancel: A cancellable + * + * Loads an image into the cache if not yet present. It always + * reports success via the `image-loaded` signal. + */ +void +phosh_background_cache_fetch_async (PhoshBackgroundCache *self, + GFile *file, + GCancellable *cancel, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshBackgroundImage *image; + g_autoptr (GTask) task = NULL; + + g_return_if_fail (PHOSH_IS_BACKGROUND_CACHE (self)); + g_return_if_fail (G_IS_FILE (file)); + g_return_if_fail (cancel == NULL || G_IS_CANCELLABLE (cancel)); + + task = g_task_new (self, cancel, callback, user_data); + g_task_set_source_tag (task, phosh_background_cache_fetch_async); + + image = g_hash_table_lookup (self->background_images, file); + if (image) { + g_debug ("Background cache hit for %s", g_file_peek_path (file)); + g_task_return_pointer (task, g_object_ref (image), g_object_unref); + } else { + g_debug ("Background cache miss for %s", g_file_peek_path (file)); + phosh_background_image_new (file, cancel, on_background_image_loaded, g_steal_pointer (&task)); + } +} + +/** + * phosh_background_cache_fetch_finish: + * @self: The background cache + * @res: The Result + * @cancel: A cancellable + * + * Finished the async operation started with `phosh_background_cache_fetch_async`. + * + * Returns; The laoded image or `NULL` on error + */ +PhoshBackgroundImage * +phosh_background_cache_fetch_finish (PhoshBackgroundCache *self, + GAsyncResult *res, + GError **error) +{ + g_assert (PHOSH_IS_BACKGROUND_CACHE (self)); + g_assert (G_IS_TASK (res)); + g_assert (!error || !*error); + + return g_task_propagate_pointer (G_TASK (res), error); +} + +/** + * phosh_background_cache_lookup_background: + * @self: The background cache + * @file: The file to lookup + * + * Looks up an image in the cache. If missing returns %NULL. + * + * Returns:(transfer none)(nullable): The looked up background + */ +PhoshBackgroundImage * +phosh_background_cache_lookup_background (PhoshBackgroundCache *self, GFile *file) +{ + g_return_val_if_fail (PHOSH_IS_BACKGROUND_CACHE (self), NULL); + g_return_val_if_fail (G_IS_FILE (file), NULL); + + return g_hash_table_lookup (self->background_images, file); +} + +/** + * phosh_background_cache_remove: + * @self: The background cache + * @file: The background to remove + * + * Drop the background identified by the given file from the background cache + */ +void +phosh_background_cache_remove (PhoshBackgroundCache *self, GFile *file) +{ + gboolean success; + + g_return_if_fail (PHOSH_IS_BACKGROUND_CACHE (self)); + + success = g_hash_table_remove (self->background_images, file); + if (!success) + g_warning ("'%s' not found in cache", g_file_peek_path (file)); +} + +/** + * phosh_background_cache_clear_all: + * @self: The background cache + * + * Drop all files from the cache. + */ +void +phosh_background_cache_clear_all (PhoshBackgroundCache *self) +{ + g_return_if_fail (PHOSH_IS_BACKGROUND_CACHE (self)); + + g_debug ("Clearing background image cache"); + g_hash_table_remove_all (self->background_images); +} diff --git a/src/background-cache.h b/src/background-cache.h new file mode 100644 index 000000000..b62c9ec1a --- /dev/null +++ b/src/background-cache.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "background-image.h" + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BACKGROUND_CACHE (phosh_background_cache_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBackgroundCache, phosh_background_cache, PHOSH, BACKGROUND_CACHE, GObject) + +PhoshBackgroundCache *phosh_background_cache_get_default (void); +void phosh_background_cache_fetch_async (PhoshBackgroundCache *self, + GFile *file, + GCancellable *cancel, + GAsyncReadyCallback callback, + gpointer user_data); +PhoshBackgroundImage * phosh_background_cache_fetch_finish (PhoshBackgroundCache *self, + GAsyncResult *res, + GError **error); +PhoshBackgroundImage *phosh_background_cache_lookup_background (PhoshBackgroundCache *self, + GFile *file); +void phosh_background_cache_remove (PhoshBackgroundCache *self, + GFile *file); +void phosh_background_cache_clear_all (PhoshBackgroundCache *self); + +G_END_DECLS diff --git a/src/background-image.c b/src/background-image.c new file mode 100644 index 000000000..bec2e0646 --- /dev/null +++ b/src/background-image.c @@ -0,0 +1,243 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-background-image" + +#include "phosh-config.h" + +#include "background-image.h" + +#include +#include + +/** + * PhoshBackgroundImage: + * + * An image for a [type@Background] that can be loaded async via [type@BackgroundCache]. + */ +enum { + PROP_0, + PROP_FILE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBackgroundImage { + GObject parent; + + GFile *file; + GdkPixbuf *pixbuf; + GTimer *load_timer; +}; + +static void initable_iface_init (GInitableIface *iface); +static void async_initable_iface_init (GAsyncInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshBackgroundImage, phosh_background_image, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_initable_iface_init)); + + +static gboolean +initable_init (GInitable *initable, GCancellable *cancel, GError **error) +{ + PhoshBackgroundImage *self = PHOSH_BACKGROUND_IMAGE (initable); + GError *local_error = NULL; + g_autoptr (GdkPixbuf) rotated = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GFileInputStream) stream = NULL; + + stream = g_file_read (self->file, cancel, &local_error); + if (stream == NULL) { + g_propagate_error (error, local_error); + return FALSE; + } + + pixbuf = gdk_pixbuf_new_from_stream (G_INPUT_STREAM (stream), cancel, &local_error); + if (pixbuf == NULL) { + g_propagate_error (error, local_error); + return FALSE; + } + + rotated = gdk_pixbuf_apply_embedded_orientation (pixbuf); + if (rotated != NULL) + g_set_object (&pixbuf, rotated); + + self->pixbuf = g_steal_pointer (&pixbuf); + + g_timer_stop (self->load_timer); + g_debug ("Background load took %.2fs", g_timer_elapsed (self->load_timer, NULL)); + + return TRUE; +} + + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = initable_init; +} + + +static void +async_initable_iface_init (GAsyncInitableIface *iface) +{ + /* Use default: run initable_init in thread */ +} + + +static void +phosh_background_image_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBackgroundImage *self = PHOSH_BACKGROUND_IMAGE (object); + + switch (property_id) { + case PROP_FILE: + self->file = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_background_image_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBackgroundImage *self = PHOSH_BACKGROUND_IMAGE (object); + + switch (property_id) { + case PROP_FILE: + g_value_set_object (value, self->file); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_background_image_finalize (GObject *object) +{ + PhoshBackgroundImage *self = PHOSH_BACKGROUND_IMAGE (object); + + g_clear_object (&self->file); + g_clear_object (&self->pixbuf); + g_clear_pointer (&self->load_timer, g_timer_destroy); + + G_OBJECT_CLASS (phosh_background_image_parent_class)->finalize (object); +} + + +static void +phosh_background_image_class_init (PhoshBackgroundImageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_background_image_get_property; + object_class->set_property = phosh_background_image_set_property; + object_class->finalize = phosh_background_image_finalize; + + props[PROP_FILE] = + g_param_spec_object ("file", "", "", + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_background_image_init (PhoshBackgroundImage *self) +{ + self->load_timer = g_timer_new (); +} + + +PhoshBackgroundImage * +phosh_background_image_new_sync (GFile *file, GCancellable *cancel, GError **error) +{ + return PHOSH_BACKGROUND_IMAGE (g_initable_new (PHOSH_TYPE_BACKGROUND_IMAGE, + cancel, + error, + "file", file, + NULL)); +} + + +void +phosh_background_image_new (GFile *file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_return_if_fail (G_IS_FILE (file)); + + g_async_initable_new_async (PHOSH_TYPE_BACKGROUND_IMAGE, + G_PRIORITY_DEFAULT, + cancellable, + callback, + user_data, + "file", file, + NULL); +} + + +PhoshBackgroundImage * +phosh_background_image_new_finish (GAsyncResult *res, GError **error) +{ + g_autoptr (GObject) source_object = NULL; + GObject *object; + + source_object = g_async_result_get_source_object (res); + g_assert (source_object != NULL); + + object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), res, error); + + return PHOSH_BACKGROUND_IMAGE (object); +} + +/** + * phosh_background_image_get_pixbuf: + * @self: The background image + * + * Gets the background image's pixbuf. + * + * Returns:(transfer none): The pixbuf + */ +GdkPixbuf * +phosh_background_image_get_pixbuf (PhoshBackgroundImage *self) +{ + g_return_val_if_fail (PHOSH_IS_BACKGROUND_IMAGE (self), FALSE); + + return self->pixbuf; +} + +/** + * phosh_background_image_get_file: + * @self: The background image + * + * Gets the file the image was loaded from + * + * Returns:(transfer none): The file + */ +GFile * +phosh_background_image_get_file (PhoshBackgroundImage *self) +{ + g_return_val_if_fail (PHOSH_IS_BACKGROUND_IMAGE (self), FALSE); + + return self->file; +} diff --git a/src/background-image.h b/src/background-image.h new file mode 100644 index 000000000..791acb7ba --- /dev/null +++ b/src/background-image.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BACKGROUND_IMAGE (phosh_background_image_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBackgroundImage, phosh_background_image, PHOSH, BACKGROUND_IMAGE, GObject) + +PhoshBackgroundImage *phosh_background_image_new_sync (GFile *file, + GCancellable *cancellable, + GError **error); +void phosh_background_image_new (GFile *file, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +PhoshBackgroundImage *phosh_background_image_new_finish (GAsyncResult *res, + GError **error); +GdkPixbuf *phosh_background_image_get_pixbuf (PhoshBackgroundImage *self); +GFile *phosh_background_image_get_file (PhoshBackgroundImage *self); + + + +G_END_DECLS diff --git a/src/background-manager.c b/src/background-manager.c new file mode 100644 index 000000000..a350349f8 --- /dev/null +++ b/src/background-manager.c @@ -0,0 +1,561 @@ +/* + * Copyright (C) 2018-2022 Purism SPC + * 2023-2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-background-manager" + +#include "background-manager.h" +#include "background.h" +#include "background-cache.h" +#include "layersurface-priv.h" +#include "manager.h" +#include "monitor/monitor.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "util.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include +#include + +#include +#include + +#include +#include + +#define BG_KEY_PRIMARY_COLOR "primary-color" +#define BG_KEY_PICTURE_OPTIONS "picture-options" +#define BG_KEY_PICTURE_URI "picture-uri" +#define BG_KEY_PICTURE_URI_DARK "picture-uri-dark" + +#define IF_KEY_COLOR_SCHEME "color-scheme" + +/** + * PhoshBackgroundManager: + * + * `PhoshBackgroundManager` keeps tracks of [type@PhoshMonitor]s to + * create [type@PhoshBackground]s that are responsible for rendering + * the background (or wallpaper). Whenever either the monitors' + * configuration or the configured wallpaper properties change the + * backgrounds are notified to update their contents. + */ + +enum { + CONFIG_CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + + +struct _PhoshBackgroundManager { + PhoshManager parent; + + PhoshMonitor *primary_monitor; + GHashTable *backgrounds; /* key: PhoshMonitor, value: PhoshBackground */ + + GDesktopBackgroundStyle style; + GnomeBGSlideShow *slideshow; + GFile *file; /* Background XML or image */ + GFileMonitor *monitor; /* Monitors file */ + GdkRGBA color; + GSettings *settings; + GSettings *interface_settings; + + GCancellable *cancel_load; +}; + +G_DEFINE_TYPE (PhoshBackgroundManager, phosh_background_manager, PHOSH_TYPE_MANAGER); + + +static void +update_background (gpointer key, gpointer value, gpointer user_data) +{ + PhoshBackground *background = PHOSH_BACKGROUND (value); + + phosh_background_needs_update (background); +} + + +static void +update_all_backgrounds (PhoshBackgroundManager *self) +{ + g_hash_table_foreach (self->backgrounds, update_background, self); + + /* We only notify config changed and don't append any background data as the specific data + depends on the surface it applies to (e.g. it's size) */ + g_signal_emit (self, signals[CONFIG_CHANGED], 0); +} + + +static void +on_slideshow_loaded (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GnomeBGSlideShow) slideshow = GNOME_BG_SLIDE_SHOW (source_object); + PhoshBackgroundManager *self = PHOSH_BACKGROUND_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (GNOME_BG_IS_SLIDE_SHOW (slideshow)); + g_return_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self)); + + if (!g_task_propagate_boolean (G_TASK (res), &err)) { + phosh_async_error_warn (err, "Failed to load %s", g_file_peek_path (self->file)); + return; + } + + self->slideshow = g_steal_pointer (&slideshow); + update_all_backgrounds (self); +} + + +static void +load_slideshow (PhoshBackgroundManager *self) +{ + g_autoptr (GError) err = NULL; + g_autofree char *path = NULL; + GnomeBGSlideShow *slideshow; + + g_return_if_fail (G_IS_FILE (self->file)); + + path = g_file_get_path (self->file); + if (!path) { + g_warning ("Couldn't get filename for %s: %s", path, err->message); + return; + } + + g_debug ("Loading slideshow '%s'", path); + slideshow = gnome_bg_slide_show_new (path); + + g_cancellable_cancel (self->cancel_load); + self->cancel_load = g_cancellable_new (); + gnome_bg_slide_show_load_async (slideshow, self->cancel_load, on_slideshow_loaded, self); +} + + +static gboolean +is_slideshow (PhoshBackgroundManager *self) +{ + if (!self->file) + return FALSE; + + return g_str_has_suffix (g_file_peek_path (self->file), ".xml"); +} + + +static void +color_from_string (GdkRGBA *color, const char *string) +{ + if (!gdk_rgba_parse (color, string)) + gdk_rgba_parse (color, "black"); +} + + +static void +refresh (PhoshBackgroundManager *self) +{ + if (is_slideshow (self)) { + load_slideshow (self); + } else { + /* Single file backed image or no image at all */ + update_all_backgrounds (self); + } +} + + +static void +on_file_changed (PhoshBackgroundManager *self, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GFileMonitor *monitor) +{ + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + + if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) + return; + + g_warning ("Background file changed, clearing cache"); + phosh_background_cache_remove (cache, self->file); + + refresh (self); +} + + +static void +monitor_file (PhoshBackgroundManager *self) +{ + g_autoptr (GError) err = NULL; + g_clear_object (&self->monitor); + + if (!self->file) + return; + + self->monitor = g_file_monitor_file (self->file, G_FILE_MONITOR_NONE, NULL, &err); + if (!self->monitor) { + g_autofree char *uri = g_file_get_uri (self->file); + + g_warning ("Failed to setup file monitor for %s: %s", uri, err->message); + return; + } + + g_signal_connect_object (self->monitor, "changed", + G_CALLBACK (on_file_changed), + self, + G_CONNECT_SWAPPED); +} + + +static void +on_settings_changed (PhoshBackgroundManager *self) +{ + GdkRGBA color; + GDesktopBackgroundStyle style; + g_autofree char *color_name = NULL; + g_autofree char *val = NULL; + g_autoptr (GFile) file = NULL; + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + GDesktopColorScheme scheme; + const char *key; + + style = g_settings_get_enum (self->settings, BG_KEY_PICTURE_OPTIONS); + color_name = g_settings_get_string (self->settings, BG_KEY_PRIMARY_COLOR); + color_from_string (&color, color_name); + + scheme = g_settings_get_enum (self->interface_settings, IF_KEY_COLOR_SCHEME); + key = scheme == G_DESKTOP_COLOR_SCHEME_PREFER_DARK ? BG_KEY_PICTURE_URI_DARK : BG_KEY_PICTURE_URI; + val = g_settings_get_string (self->settings, key); + + if (g_str_has_prefix (val, "/") || g_str_has_prefix (val, "file:///")) + file = g_file_new_for_uri (val); + else if (g_strcmp0 (val, "")) + g_warning ("Invalid background path %s", val); + + if (self->style == style && gdk_rgba_equal (&self->color, &color) && + phosh_util_file_equal (self->file, file)) { + return; + } + + /* Clear cache if uri changed */ + if (self->file && !phosh_util_file_equal (self->file, file)) + phosh_background_cache_remove (cache, self->file); + + self->style = style; + self->color = color; + g_clear_object (&self->slideshow); + g_clear_object (&self->file); + self->file = g_steal_pointer (&file); + monitor_file (self); + + refresh (self); +} + + +static void +on_background_destroy (PhoshBackgroundManager *self, GtkWidget *widget) +{ + PhoshBackground *background = PHOSH_BACKGROUND (widget); + GHashTableIter iter; + gpointer key, value; + + g_return_if_fail (PHOSH_IS_BACKGROUND (widget)); + + g_hash_table_iter_init (&iter, self->backgrounds); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (background == PHOSH_BACKGROUND (value)) { + PhoshMonitor *monitor = PHOSH_MONITOR (key); + + g_hash_table_remove (self->backgrounds, monitor); + return; + } + } + + g_debug ("Background %p to remove not found", background); +} + + +static PhoshBackground * +create_background_for_monitor (PhoshBackgroundManager *self, PhoshMonitor *monitor) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + GtkWidget *background; + + background = phosh_background_new (phosh_wayland_get_zwlr_layer_shell_v1 (wl), + monitor, + monitor == self->primary_monitor, + ZWLR_LAYER_SHELL_V1_LAYER_BACKGROUND); + g_signal_connect_object (background, + "destroy", + G_CALLBACK (on_background_destroy), + self, + G_CONNECT_SWAPPED); + + return PHOSH_BACKGROUND (background); +} + + +static void +on_monitor_removed (PhoshBackgroundManager *self, + PhoshMonitor *monitor, + PhoshMonitorManager *monitormanager) +{ + g_return_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + if (!g_hash_table_lookup (self->backgrounds, monitor)) + return; + + g_debug ("Monitor %p removed, removing background", monitor); + g_hash_table_remove (self->backgrounds, monitor); +} + + +static void +on_monitor_configured (PhoshBackgroundManager *self, PhoshMonitor *monitor) +{ + PhoshBackground *background; + float scale; + + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + scale = phosh_monitor_get_fractional_scale (monitor); + g_debug ("Monitor %p (%s) configured, scale %f", monitor, monitor->name, scale); + + background = g_hash_table_lookup (self->backgrounds, monitor); + if (background == NULL) { + background = create_background_for_monitor (self, monitor); + g_hash_table_insert (self->backgrounds, g_object_ref (monitor), background); + } else { + phosh_background_needs_update (background); + } + + gtk_widget_set_visible (GTK_WIDGET (background), TRUE); +} + + +static void +on_monitor_added (PhoshBackgroundManager *self, + PhoshMonitor *monitor, + PhoshMonitorManager *unused) +{ + g_return_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + g_debug ("Monitor %p added", monitor); + + g_signal_connect_object (monitor, "configured", + G_CALLBACK (on_monitor_configured), + self, + G_CONNECT_SWAPPED); + if (phosh_monitor_is_configured (monitor)) + on_monitor_configured (self, monitor); +} + + +static void +on_primary_monitor_changed (PhoshBackgroundManager *self, + GParamSpec *pspec, + PhoshShell *shell) +{ + PhoshBackground *background; + PhoshMonitor *monitor; + + g_return_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SHELL (shell)); + + monitor = phosh_shell_get_primary_monitor (shell); + if (monitor == self->primary_monitor) + return; + + if (self->primary_monitor) { + background = g_hash_table_lookup (self->backgrounds, self->primary_monitor); + if (background) + phosh_background_set_primary (background, FALSE); + } + + g_set_object (&self->primary_monitor, monitor); + + if (monitor) { + background = g_hash_table_lookup (self->backgrounds, monitor); + if (background) + phosh_background_set_primary (background, TRUE); + } +} + + +static void +phosh_background_manager_idle_init (PhoshManager *manager) +{ + PhoshBackgroundManager *self = PHOSH_BACKGROUND_MANAGER (manager); + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitorManager *monitor_manager = phosh_shell_get_monitor_manager (shell); + + self->settings = g_settings_new ("org.gnome.desktop.background"); + g_object_connect (self->settings, + "swapped-signal::changed::" BG_KEY_PICTURE_URI, on_settings_changed, self, + "swapped-signal::changed::" BG_KEY_PICTURE_URI_DARK, on_settings_changed, self, + "swapped-signal::changed::" BG_KEY_PICTURE_OPTIONS, on_settings_changed, self, + "swapped-signal::changed::" BG_KEY_PRIMARY_COLOR, on_settings_changed, self, + NULL); + self->interface_settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect_swapped (self->interface_settings, + "changed::" IF_KEY_COLOR_SCHEME, + G_CALLBACK (on_settings_changed), + self); + /* Fill in initial values */ + on_settings_changed (self); + + /* Listen for monitor changes */ + g_object_connect (monitor_manager, + "swapped-object-signal::monitor-added", on_monitor_added, self, + "swapped-object-signal::monitor-removed", on_monitor_removed, self, + NULL); + g_signal_connect_swapped (shell, "notify::primary-monitor", + G_CALLBACK (on_primary_monitor_changed), + self); + self->primary_monitor = g_object_ref (phosh_shell_get_primary_monitor (shell)); + + /* catch up with monitors already present */ + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (monitor_manager); i++) { + PhoshMonitor *monitor = phosh_monitor_manager_get_monitor (monitor_manager, i); + + on_monitor_added (self, monitor, NULL); + } +} + + +static void +phosh_background_manager_finalize (GObject *object) +{ + PhoshBackgroundManager *self = PHOSH_BACKGROUND_MANAGER (object); + + g_cancellable_cancel (self->cancel_load); + g_clear_object (&self->cancel_load); + + g_hash_table_destroy (self->backgrounds); + g_clear_object (&self->primary_monitor); + g_clear_object (&self->settings); + g_clear_object (&self->interface_settings); + g_clear_object (&self->slideshow); + g_clear_object (&self->monitor); + g_clear_object (&self->file); + + G_OBJECT_CLASS (phosh_background_manager_parent_class)->finalize (object); +} + + +static void +phosh_background_manager_class_init (PhoshBackgroundManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->finalize = phosh_background_manager_finalize; + + manager_class->idle_init = phosh_background_manager_idle_init; + + /** + * PhoshBackgroundManager::config-changed: + * @self: The backgroundd manager + * + */ + signals[CONFIG_CHANGED] = + g_signal_new ("config-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + + +static void +phosh_background_manager_init (PhoshBackgroundManager *self) +{ + self->backgrounds = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + g_object_unref, + (GDestroyNotify)gtk_widget_destroy); +} + +PhoshBackgroundManager * +phosh_background_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_BACKGROUND_MANAGER, NULL); +} + +/** + * phosh_background_manager_get_backgrounds: + * @self: The #PhoshBackgroundManager + * + * Returns:(transfer container) (element-type PhoshBackground): The current backgrounds + */ +GList * +phosh_background_manager_get_backgrounds (PhoshBackgroundManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self), NULL); + + return g_hash_table_get_values (self->backgrounds); +} + +/** + * phosh_background_manager_get_data: (skip) + * @self: The background manager + * @background: The background to fetch information fore + * + * Get the data that allows a [type@Background] to build it's + * image. This is the single place that determines this so other parts + * don't need to care whether we handle a slide or a single image. + * + * Returns:(transfer full)(nullable): The data allowing to build a background image + */ +PhoshBackgroundData * +phosh_background_manager_get_data (PhoshBackgroundManager *self, PhoshBackground *background) +{ + g_autoptr (PhoshBackgroundData) bg_data = g_new0 (PhoshBackgroundData, 1); + + g_return_val_if_fail (PHOSH_IS_BACKGROUND_MANAGER (self), NULL); + g_return_val_if_fail (PHOSH_IS_BACKGROUND (background), NULL); + + *bg_data = (PhoshBackgroundData) { + .color = self->color, + .style = self->style, + }; + + /* Slideshow did not load successfully */ + if (is_slideshow (self) && !self->slideshow) + return g_steal_pointer (&bg_data); + + if (self->slideshow) { + gint width, height; + gboolean fixed; + const char *file1; + + width = phosh_layer_surface_get_configured_width (PHOSH_LAYER_SURFACE (background)); + height = phosh_layer_surface_get_configured_height (PHOSH_LAYER_SURFACE (background)); + + if (width <= 0 || height <= 0) { + g_critical ("Layer surface not yet configured"); + return g_steal_pointer (&bg_data); + } + + g_assert (GNOME_BG_IS_SLIDE_SHOW (self->slideshow)); + + /* TODO: handle actual slideshows (fixed == false) */ + gnome_bg_slide_show_get_slide (self->slideshow, 0, width, height, + NULL, NULL, &fixed, &file1, NULL); + g_debug ("Background file: %s, fixed: %d", file1, fixed); + if (!fixed) + g_warning ("Only fixed slideshows supported properly atm"); + + bg_data->uri = g_file_new_for_path (file1); + } else if (self->file) { + bg_data->uri = g_object_ref (self->file); + } + + return g_steal_pointer (&bg_data); +} diff --git a/src/background-manager.h b/src/background-manager.h new file mode 100644 index 000000000..1f7c07911 --- /dev/null +++ b/src/background-manager.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "background.h" +#include "manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BACKGROUND_MANAGER (phosh_background_manager_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshBackgroundManager, + phosh_background_manager, + PHOSH, + BACKGROUND_MANAGER, + PhoshManager) + +PhoshBackgroundManager *phosh_background_manager_new (void); +GList *phosh_background_manager_get_backgrounds (PhoshBackgroundManager *self); +PhoshBackgroundData *phosh_background_manager_get_data (PhoshBackgroundManager *self, + PhoshBackground *background); + +G_END_DECLS diff --git a/src/background.c b/src/background.c new file mode 100644 index 000000000..19245c903 --- /dev/null +++ b/src/background.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2018-2022 Purism SPC + * 2023-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Derived in parts from GnomeBG which is + * + * Copyright (C) 2000 Eazel, Inc. + * Copyright (C) 2007-2008 Red Hat, Inc. + */ + +#define G_LOG_DOMAIN "phosh-background" + +#include "background.h" +#include "background-cache.h" +#include "background-image.h" +#include "background-manager.h" +#include "layersurface-priv.h" +#include "shell-priv.h" +#include "top-panel.h" +#include "util.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include +#include + +#include + +#include +#include + +#define COLOR_TO_PIXEL(color) ((((int)(color->red * 255)) << 24) | \ + (((int)(color->green * 255)) << 16) | \ + (((int)(color->blue * 255)) << 8) | \ + (((int)(color->alpha * 255)))) +/** + * PhoshBackground: + * + * A [type@LayerSurface] representing the background drawn on a + * [type@Monitor]. + * + * The background is updated by [type@BackgroundManager] when needed. + */ + +enum { + PROP_0, + PROP_PRIMARY, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshBackground +{ + PhoshLayerSurface parent; + + /* The cached background image */ + GFile *uri; + PhoshBackgroundImage *cached_bg_image; + GCancellable *cancel_load; + /* How the background in rendered */ + GDesktopBackgroundStyle style; + GdkRGBA color; + GdkPixbuf *pixbuf; + gboolean needs_update; + + /* The monitor backed by PhoshBackground */ + gboolean primary; + gboolean configured; +}; + + +G_DEFINE_TYPE (PhoshBackground, phosh_background, PHOSH_TYPE_LAYER_SURFACE); + + +void +phosh_background_data_free (PhoshBackgroundData *bd_data) +{ + g_clear_object (&bd_data->uri); + g_free (bd_data); +} + + +static void +phosh_background_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBackground *self = PHOSH_BACKGROUND (object); + + switch (property_id) { + case PROP_PRIMARY: + phosh_background_set_primary (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_background_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBackground *self = PHOSH_BACKGROUND (object); + + switch (property_id) { + case PROP_PRIMARY: + g_value_set_boolean (value, self->primary); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static GdkPixbuf * +pb_scale_to_fit (GdkPixbuf *src, int width, int height, GdkRGBA *color) +{ + int orig_width, orig_height; + int final_width, final_height; + int off_x, off_y; + double ratio_horiz, ratio_vert, ratio; + GdkPixbuf *bg; + + bg = gdk_pixbuf_new (GDK_COLORSPACE_RGB, FALSE, 8, width, height); + gdk_pixbuf_fill (bg, COLOR_TO_PIXEL(color)); + + orig_width = gdk_pixbuf_get_width (src); + orig_height = gdk_pixbuf_get_height (src); + ratio_horiz = (double) width / orig_width; + ratio_vert = (double) height / orig_height; + + ratio = ratio_horiz > ratio_vert ? ratio_vert : ratio_horiz; + final_width = ceil (ratio * orig_width); + final_height = ceil (ratio * orig_height); + + off_x = (width - final_width) / 2; + off_y = (height - final_height) / 2; + gdk_pixbuf_composite (src, + bg, + off_x, off_y, /* dest x,y */ + final_width, + final_height, + off_x, off_y, /* offset x, y */ + ratio, + ratio, + GDK_INTERP_BILINEAR, + 255); + return bg; +} + + +static GdkPixbuf * +image_background (PhoshBackgroundImage *image, + guint width, + guint height, + GDesktopBackgroundStyle style, + GdkRGBA *color) +{ + GdkPixbuf *scaled_bg = NULL;; + + if (image == NULL) { + g_debug ("No image, using 'none' desktop style"); + style = G_DESKTOP_BACKGROUND_STYLE_NONE; + } + + switch (style) { + case G_DESKTOP_BACKGROUND_STYLE_NONE: + /* Nothing to do */ + break; + case G_DESKTOP_BACKGROUND_STYLE_SCALED: + scaled_bg = pb_scale_to_fit (phosh_background_image_get_pixbuf (image), width, height, color); + break; + case G_DESKTOP_BACKGROUND_STYLE_WALLPAPER: + case G_DESKTOP_BACKGROUND_STYLE_CENTERED: + case G_DESKTOP_BACKGROUND_STYLE_STRETCHED: + case G_DESKTOP_BACKGROUND_STYLE_SPANNED: + g_warning ("Unimplemented style %d, using zoom", style); + G_GNUC_FALLTHROUGH; + case G_DESKTOP_BACKGROUND_STYLE_ZOOM: + default: + scaled_bg = phosh_utils_pixbuf_scale_to_min (phosh_background_image_get_pixbuf (image), + width, + height); + break; + } + + return scaled_bg; +} + + +static gboolean +phosh_background_draw (GtkWidget *widget, cairo_t *cr) +{ + PhoshBackground *self = PHOSH_BACKGROUND (widget); + int x = 0, y = 0, width, height; + + g_return_val_if_fail (PHOSH_IS_BACKGROUND (self), GDK_EVENT_PROPAGATE); + + if (!self->configured) + return GDK_EVENT_PROPAGATE; + + if (self->primary) + phosh_shell_get_usable_area (phosh_shell_get_default (), &x, &y, NULL, NULL); + + cairo_save (cr); + if (self->primary) { + /* Primary background: use CSS color as it's the top- and home-bar's background */ + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + width = gtk_widget_get_allocated_width (GTK_WIDGET (self)); + height = gtk_widget_get_allocated_height (GTK_WIDGET (self)); + gtk_render_background (context, cr, 0, 0, width, height); + } else { + cairo_set_operator (cr, CAIRO_OPERATOR_OVER); + cairo_set_source_rgb (cr, self->color.red, self->color.green, self->color.blue); + cairo_paint (cr); + } + + if (self->pixbuf) { + gdk_cairo_set_source_pixbuf (cr, self->pixbuf, x, y); + cairo_paint (cr); + } + + cairo_restore (cr); + + return GDK_EVENT_PROPAGATE; +} + + +static void +update_image (PhoshBackground *self) +{ + int width, height; + + if (!self->configured) + return; + + if (self->primary) { + phosh_shell_get_usable_area (phosh_shell_get_default (), NULL, NULL, &width, &height); + } else { + width = phosh_layer_surface_get_configured_width (PHOSH_LAYER_SURFACE (self)); + height = phosh_layer_surface_get_configured_height (PHOSH_LAYER_SURFACE (self)); + } + + g_return_if_fail (width > 0 && height > 0); + + g_debug ("Scaling background %p to %dx%d", self, width, height); + + g_clear_object (&self->pixbuf); + self->pixbuf = image_background (self->cached_bg_image, width, height, + self->style, &self->color); + + self->needs_update = FALSE; + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +static void +on_background_cache_fetch_ready (GObject *source_object, GAsyncResult *res, gpointer data) +{ + g_autoptr (GError) err = NULL; + g_autoptr (PhoshBackgroundImage) image = NULL; + PhoshBackground *self = PHOSH_BACKGROUND (data); + PhoshBackgroundCache *cache = PHOSH_BACKGROUND_CACHE (source_object); + + image = phosh_background_cache_fetch_finish (cache, res, &err); + if (!image) { + phosh_async_error_warn (err, "Failed to load background image"); + return; + } + + g_assert (PHOSH_IS_BACKGROUND (self)); + g_assert (PHOSH_IS_BACKGROUND_CACHE (cache)); + + g_set_object (&self->cached_bg_image, image); + update_image (self); +} + + +static void +trigger_update (PhoshBackground *self) +{ + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + PhoshBackgroundManager *manager = phosh_shell_get_background_manager (phosh_shell_get_default ()); + g_autoptr (PhoshBackgroundData) bg_data = NULL; + + g_debug ("Updating Background %p", self); + bg_data = phosh_background_manager_get_data (manager, self); + + self->needs_update = TRUE; + self->style = bg_data->style; + self->color = bg_data->color; + g_set_object (&self->uri, bg_data->uri); + + g_cancellable_cancel (self->cancel_load); + g_clear_object (&self->cancel_load); + self->cancel_load = g_cancellable_new (); + + if (self->uri) { + phosh_background_cache_fetch_async (cache, + self->uri, + self->cancel_load, + on_background_cache_fetch_ready, + self); + } else { + g_clear_object (&self->cached_bg_image); + update_image (self); + } +} + + +static void +phosh_background_configured (PhoshLayerSurface *layer_surface) +{ + PhoshBackground *self = PHOSH_BACKGROUND (layer_surface); + + PHOSH_LAYER_SURFACE_CLASS (phosh_background_parent_class)->configured (layer_surface); + + self->configured = TRUE; + trigger_update (self); +} + + +static void +phosh_background_finalize (GObject *object) +{ + PhoshBackground *self = PHOSH_BACKGROUND (object); + + g_cancellable_cancel (self->cancel_load); + g_clear_object (&self->cancel_load); + g_clear_object (&self->pixbuf); + g_clear_object (&self->cached_bg_image); + + G_OBJECT_CLASS (phosh_background_parent_class)->finalize (object); +} + + +static void +phosh_background_class_init (PhoshBackgroundClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + + object_class->finalize = phosh_background_finalize; + object_class->set_property = phosh_background_set_property; + object_class->get_property = phosh_background_get_property; + + widget_class->draw = phosh_background_draw; + + layer_surface_class->configured = phosh_background_configured; + + /** + * PhoshBackground:primary: + * + * Whether this is the background for the primary monitor. + */ + props[PROP_PRIMARY] = + g_param_spec_boolean ("primary", + "Primary", + "Primary monitor", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_CONSTRUCT); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "phosh-background"); +} + + +static void +phosh_background_init (PhoshBackground *self) +{ +} + + +GtkWidget * +phosh_background_new (gpointer layer_shell, + PhoshMonitor *monitor, + gboolean primary, + guint layer) +{ + return g_object_new (PHOSH_TYPE_BACKGROUND, + "layer-shell", layer_shell, + "wl-output", monitor->wl_output, + "anchor", (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT), + "layer", layer, + "kbd-interactivity", FALSE, + "exclusive-zone", -1, + "namespace", "phosh background", + "primary", primary, + NULL); +} + + +void +phosh_background_set_primary (PhoshBackground *self, gboolean primary) +{ + if (self->primary == primary) + return; + + self->primary = primary; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRIMARY]); + + trigger_update (self); +} + +/** + * phosh_background_needs_update: + * @self: The background + * + * Marks the background's data as dirty, needing an update. This will make the + * `PhoshBackground` update it's background image. + */ +void +phosh_background_needs_update (PhoshBackground *self) +{ + g_return_if_fail (PHOSH_IS_BACKGROUND (self)); + + /* Skip update if layer surface isn't yet configured, it will + * trigger on layer_surface.configured anyway */ + if (phosh_layer_surface_get_configured_width (PHOSH_LAYER_SURFACE (self)) <= 0 || + phosh_layer_surface_get_configured_height (PHOSH_LAYER_SURFACE (self)) <= 0) { + return; + } + + trigger_update (self); +} diff --git a/src/background.h b/src/background.h new file mode 100644 index 000000000..925dfe914 --- /dev/null +++ b/src/background.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "layersurface.h" +#include "monitor/monitor.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BACKGROUND (phosh_background_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshBackground, phosh_background, PHOSH, BACKGROUND, PhoshLayerSurface) + +typedef struct _PhoshBackgroundData { + GFile *uri; + GdkRGBA color; + GDesktopBackgroundStyle style; +} PhoshBackgroundData; + +GtkWidget *phosh_background_new (gpointer layer_shell, + PhoshMonitor *monitor, + gboolean primary, + guint layer); +void phosh_background_set_primary (PhoshBackground *self, + gboolean primary); +float phosh_background_get_scale (PhoshBackground *self); +void phosh_background_set_scale (PhoshBackground *self, + float scale); +void phosh_background_needs_update (PhoshBackground *self); + +void phosh_background_data_free (PhoshBackgroundData *bg_data); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshBackgroundData, phosh_background_data_free) + +G_END_DECLS diff --git a/src/backlight-priv.h b/src/backlight-priv.h new file mode 100644 index 000000000..f238abb71 --- /dev/null +++ b/src/backlight-priv.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "backlight.h" + +#include + +G_BEGIN_DECLS + +struct _PhoshBacklightClass { + GObjectClass parent_class; + + void (* set_level) (PhoshBacklight *backlight, + int brightness_target, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + + int (* set_level_finish) (PhoshBacklight *backlight, + GAsyncResult *result, + GError **error); +}; + +void phosh_backlight_backend_update_level (PhoshBacklight *self, int level); +void phosh_backlight_set_range (PhoshBacklight *self, int min, int max, PhoshBacklightScale scale); +void phosh_backlight_set_name (PhoshBacklight *self, const char *name); + +G_END_DECLS diff --git a/src/backlight-sysfs.c b/src/backlight-sysfs.c new file mode 100644 index 000000000..40a965626 --- /dev/null +++ b/src/backlight-sysfs.c @@ -0,0 +1,326 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Somewhat based on gsd-backlight which is + * Copyright (C) 2018 Red Hat Inc. + */ + +#define G_LOG_DOMAIN "phosh-backlight-sysfs" + +#include "phosh-config.h" + +#include "backlight-sysfs.h" +#include "dbus/login1-session-dbus.h" +#include "udev-manager.h" + +#include + +/** + * PhoshBacklightSysfs: + * + * A backlight managed via sysfs / logind + */ + +enum { + PROP_0, + PROP_CONNECTOR_NAME, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBacklightSysfs { + PhoshBacklight parent; + + char *connector_name; + + char *device_name; + char *device_path; + char *brightness_path; + + GUdevDevice *device; + PhoshDBusLoginSession *session_proxy; +}; + +static void initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshBacklightSysfs, phosh_backlight_sysfs, PHOSH_TYPE_BACKLIGHT, + G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init)) + +static void +on_dbus_login_session_brightness_set (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + PhoshDBusLoginSession *session_proxy = PHOSH_DBUS_LOGIN_SESSION (source_object); + g_autoptr (GTask) task = G_TASK (user_data); + int brightness; + g_autoptr (GError) error = NULL; + + brightness = GPOINTER_TO_INT (g_task_get_task_data (task)); + if (!phosh_dbus_login_session_call_set_brightness_finish (session_proxy, + res, + &error)) { + g_task_return_error (task, g_steal_pointer (&error)); + return; + } + + g_task_return_int (task, brightness); +} + + +static int +phosh_backlight_sysfs_set_level_finish (PhoshBacklight *backlight, + GAsyncResult *result, + GError **error) +{ + PhoshBacklightSysfs *self = PHOSH_BACKLIGHT_SYSFS (backlight); + + g_return_val_if_fail (g_task_is_valid (result, self), -1); + + return g_task_propagate_int (G_TASK (result), error); +} + + +static void +phosh_backlight_sysfs_set_level (PhoshBacklight *backlight, + int brightness, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshBacklightSysfs *self = PHOSH_BACKLIGHT_SYSFS (backlight); + g_autoptr (GTask) task = NULL; + + g_return_if_fail (PHOSH_IS_BACKLIGHT_SYSFS (self)); + + task = g_task_new (self, cancellable, callback, user_data); + g_task_set_task_data (task, GINT_TO_POINTER (brightness), NULL); + g_task_set_source_tag (task, phosh_backlight_sysfs_set_level); + + if (!self->session_proxy) + return; + + g_debug ("Setting brightness via logind: %d", brightness); + phosh_dbus_login_session_call_set_brightness (self->session_proxy, + "backlight", + self->device_name, + brightness, + cancellable, + on_dbus_login_session_brightness_set, + g_steal_pointer (&task)); +} + + +static void +phosh_backlight_sysfs_update (PhoshBacklightSysfs *self) +{ + g_autoptr (GError) err = NULL; + g_autofree char *contents = NULL; + int level; + + if (!g_file_get_contents (self->brightness_path, &contents, NULL, &err)) { + g_warning ("Backlight %s: Could not get brightness from sysfs: %s", + self->connector_name, + err->message); + return; + } + + level = g_ascii_strtoll (contents, NULL, 0); + phosh_backlight_backend_update_level (PHOSH_BACKLIGHT (self), level); +} + + +static void +on_backlight_changed (PhoshBacklightSysfs *self, + GUdevDevice *udev_device, + PhoshUdevManager *udev_manager) +{ + g_assert (PHOSH_IS_BACKLIGHT_SYSFS (self)); + g_assert (PHOSH_IS_UDEV_MANAGER (udev_manager)); + + if (g_strcmp0 (g_udev_device_get_sysfs_path (udev_device), self->device_path) != 0) + return; + + phosh_backlight_sysfs_update (self); +} + + +static gboolean +phosh_backlight_sysfs_get_udev_info (GUdevDevice *device, + int *minout, + int *maxout, + PhoshBacklightScale *scaleout, + GError **err) +{ + int min, max; + const char *device_type, *scale_str; + PhoshBacklightScale scale = PHOSH_BACKLIGHT_SCALE_UNKNOWN; + + max = g_udev_device_get_sysfs_attr_as_int (device, "max_brightness"); + min = MAX (1, max / 100); + + /* If the interface has less than 100 possible values, and it is of type + * raw, then assume that 0 does not turn off the backlight completely. */ + device_type = g_udev_device_get_sysfs_attr (device, "type"); + if (max < 99 && g_strcmp0 (device_type, "raw") == 0) + min = 0; + + /* Ignore a backlight which has no steps. */ + if (min >= max) { + g_set_error (err, G_IO_ERROR, G_IO_ERROR_FAILED, + "Backlight has an invalid maximum brightness [%d,%d]", min, max); + return FALSE; + } + + scale_str = g_udev_device_get_sysfs_attr (device, "scale"); + if (scale_str) { + if (g_str_equal (scale_str, "unknown")) { + scale = PHOSH_BACKLIGHT_SCALE_UNKNOWN; + } else if (g_str_equal (scale_str, "linear")) { + scale = PHOSH_BACKLIGHT_SCALE_LINEAR; + } else if (g_str_equal (scale_str, "non-linear")) { + scale = PHOSH_BACKLIGHT_SCALE_NON_LINEAR; + } else { + g_warning ("Unknown brightness scale '%s'", scale_str); + } + } + + if (minout) + *minout = min; + if (maxout) + *maxout = max; + if (scaleout) + *scaleout = scale; + + return TRUE; +} + + +static gboolean +initable_init (GInitable *initable, GCancellable *cancel, GError **error) +{ + PhoshBacklightSysfs *self = PHOSH_BACKLIGHT_SYSFS (initable); + PhoshUdevManager *udev_manager = phosh_udev_manager_get_default (); + PhoshBacklightScale scale; + int min = 0, max = 0; + + if (!self->connector_name) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, + "No connector name given"); + return FALSE; + } + + self->device = phosh_udev_manager_find_backlight (udev_manager, self->connector_name); + if (!self->device) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, + "No matching backlight device found"); + return FALSE; + } + + if (!phosh_backlight_sysfs_get_udev_info (self->device, &min, &max, &scale, error)) + return FALSE; + + phosh_backlight_set_range (PHOSH_BACKLIGHT (self), min, max, scale); + + self->device_name = g_strdup (g_udev_device_get_name (self->device)); + self->device_path = realpath (g_udev_device_get_sysfs_path (self->device), NULL); + if (!self->device_path) { + g_set_error (error, G_IO_ERROR, + g_io_error_from_errno (errno), + "Could not get real path for %s", self->device_name); + return FALSE; + } + self->brightness_path = g_build_filename (self->device_path, "brightness", NULL); + self->session_proxy = phosh_udev_manager_get_session_proxy (udev_manager); + + g_signal_connect_object (udev_manager, "backlight-changed", + G_CALLBACK (on_backlight_changed), + self, + G_CONNECT_SWAPPED); + phosh_backlight_sysfs_update (self); + + return TRUE; +} + + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = initable_init; +} + + +static void +phosh_backlight_sysfs_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBacklightSysfs *self = PHOSH_BACKLIGHT_SYSFS (object); + + switch (property_id) { + case PROP_CONNECTOR_NAME: + self->connector_name = g_value_dup_string (value); + phosh_backlight_set_name (PHOSH_BACKLIGHT (self), self->connector_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_backlight_sysfs_dispose (GObject *object) +{ + PhoshBacklightSysfs *self = PHOSH_BACKLIGHT_SYSFS (object); + + g_clear_pointer (&self->brightness_path, g_free); + g_clear_pointer (&self->device_path, g_free); + g_clear_pointer (&self->device_name, g_free); + g_clear_pointer (&self->connector_name, g_free); + g_clear_object (&self->device); + g_clear_object (&self->session_proxy); + + G_OBJECT_CLASS (phosh_backlight_sysfs_parent_class)->dispose (object); +} + + +static void +phosh_backlight_sysfs_class_init (PhoshBacklightSysfsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshBacklightClass *backlight_class = PHOSH_BACKLIGHT_CLASS (klass); + + object_class->set_property = phosh_backlight_sysfs_set_property; + object_class->dispose = phosh_backlight_sysfs_dispose; + + backlight_class->set_level = phosh_backlight_sysfs_set_level; + backlight_class->set_level_finish = phosh_backlight_sysfs_set_level_finish; + + props[PROP_CONNECTOR_NAME] = + g_param_spec_string ("connector-name", "", "", + NULL, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_backlight_sysfs_init (PhoshBacklightSysfs *self) +{ +} + + +PhoshBacklightSysfs * +phosh_backlight_sysfs_new (const char *connector_name, GError **error) +{ + return g_initable_new (PHOSH_TYPE_BACKLIGHT_SYSFS, NULL, error, + "connector-name", connector_name, + NULL); +} diff --git a/src/backlight-sysfs.h b/src/backlight-sysfs.h new file mode 100644 index 000000000..2387f583a --- /dev/null +++ b/src/backlight-sysfs.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "backlight-priv.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BACKLIGHT_SYSFS (phosh_backlight_sysfs_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBacklightSysfs, phosh_backlight_sysfs, PHOSH, BACKLIGHT_SYSFS, + PhoshBacklight) + +PhoshBacklightSysfs *phosh_backlight_sysfs_new (const char *connector_name, GError **error); + +G_END_DECLS diff --git a/src/backlight.c b/src/backlight.c new file mode 100644 index 000000000..8caa78249 --- /dev/null +++ b/src/backlight.c @@ -0,0 +1,415 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-backlight" + +#define _GNU_SOURCE + +#include "phosh-config.h" + +#include "backlight-priv.h" +#include "shell-priv.h" + +#include + +#include + +/** + * PhoshBacklight: + * + * A `PhoshMonitor`'s backlight. Derived classes implement the actual + * backlight handling. + * + * The backlight's brightness levels are mapped to a logarithmic curve + * unless the backlight implementation indicates via it's `scale` that + * it does this internally already. + */ + +enum { + PROP_0, + PROP_BRIGHTNESS, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshBacklightPrivate { + GObject parent; + + char *name; + PhoshBacklightScale scale; + + gboolean pending; + + /* Brightness levels as exposed by the backlight hardware */ + struct { + int min; + int max; + int target; + } level; + + /* Brightness as exposed to API users */ + struct { + double min; + double max; + double target; + } brightness; + + GCancellable *cancel; +} PhoshBacklightPrivate; + +G_DEFINE_ABSTRACT_TYPE_WITH_PRIVATE (PhoshBacklight, phosh_backlight, G_TYPE_OBJECT) + + +static void +phosh_backlight_set_curve (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + if (priv->scale == PHOSH_BACKLIGHT_SCALE_NON_LINEAR) { + priv->brightness.max = priv->level.max; + priv->brightness.min = priv->level.min; + } else { + /* TODO: allow for display backlight specific curves */ + priv->brightness.max = log10 (priv->level.max); + priv->brightness.min = log10 (priv->level.min); + } +} + + +static double +phosh_backlight_level_to_brightness (PhoshBacklight *self, int level) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + double brightness; + + if (priv->scale == PHOSH_BACKLIGHT_SCALE_NON_LINEAR) + return level; + + brightness = log10 (level); + return CLAMP (brightness, priv->brightness.min, priv->brightness.max); +} + + +static int +phosh_backlight_brightness_to_level (PhoshBacklight *self, double brightness) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + int level; + + if (priv->scale == PHOSH_BACKLIGHT_SCALE_NON_LINEAR) + return round (brightness); + + level = round (exp10 (brightness)); + return CLAMP (level, priv->level.min, priv->level.max); +} + + +static void +phosh_backlight_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBacklight *self = PHOSH_BACKLIGHT (object); + + switch (property_id) { + case PROP_BRIGHTNESS: + phosh_backlight_set_brightness (self, g_value_get_double (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_backlight_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBacklight *self = PHOSH_BACKLIGHT (object); + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + switch (property_id) { + case PROP_BRIGHTNESS: + g_value_set_double (value, priv->brightness.target); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_backlight_dispose (GObject *object) +{ + PhoshBacklight *self = PHOSH_BACKLIGHT (object); + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_cancellable_cancel (priv->cancel); + g_clear_object (&priv->cancel); + + g_clear_pointer (&priv->name, g_free); + + G_OBJECT_CLASS (phosh_backlight_parent_class)->dispose (object); +} + + +static void +phosh_backlight_class_init (PhoshBacklightClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_backlight_get_property; + object_class->set_property = phosh_backlight_set_property; + object_class->dispose = phosh_backlight_dispose; + + props[PROP_BRIGHTNESS] = + g_param_spec_double ("brightness", "", "", + 0, G_MAXDOUBLE, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_backlight_init (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + /* Ensure initial sync */ + priv->level.target = -1; + + priv->cancel = g_cancellable_new (); +} + +/** + * phosh_backlight_backend_update_level: + * @self: The backlight + * @brightness: the brightness level + * + * This is invoked by the concrete backend implementation when the + * hardware changed brightness. + */ +void +phosh_backlight_backend_update_level (PhoshBacklight *self, int level) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + int new_level; + + if (priv->level.target == level) + return; + + new_level = CLAMP (level, priv->level.min, priv->level.max); + if (level != new_level) + g_warning ("Trying to set out-of-range brightness level %d on %s", level, priv->name); + + priv->level.target = new_level; + priv->brightness.target = phosh_backlight_level_to_brightness (self, new_level); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BRIGHTNESS]); +} + + +void +phosh_backlight_get_range (PhoshBacklight *self, int *min_brightness, int *max_brightness) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_BACKLIGHT (self)); + + if (min_brightness) + *min_brightness = priv->brightness.min; + + if (max_brightness) + *max_brightness = priv->brightness.max; +} + + +void +phosh_backlight_set_range (PhoshBacklight *self, int min, int max, PhoshBacklightScale scale) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + gboolean force_non_linear; + + priv->level.min = min; + priv->level.max = max; + priv->scale = scale; + + force_non_linear = !!(phosh_shell_get_debug_flags () & PHOSH_SHELL_DEBUG_BACKLIGHT_NON_LINEAR); + if (force_non_linear) + priv->scale = PHOSH_BACKLIGHT_SCALE_NON_LINEAR; + + if (priv->scale == PHOSH_BACKLIGHT_SCALE_NON_LINEAR) + g_debug ("Backlight brightness maps to non-linear brightness curve"); + else + g_debug ("Backlight brightness maps to linear brightness curve"); + + phosh_backlight_set_curve (self); +} + + +int +phosh_backlight_get_brightness (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_BACKLIGHT (self), -1); + + return priv->brightness.target; +} + + +static void +on_level_set (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshBacklight *self = PHOSH_BACKLIGHT (source_object); + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + g_autoptr (GError) err = NULL; + int level; + + priv->pending = FALSE; + + level = PHOSH_BACKLIGHT_GET_CLASS (self)->set_level_finish (self, res, &err); + if (level < 0) { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_warning ("Setting backlight on %s failed: %s", priv->name, err->message); + return; + } + + /* Brightness got updated from the system and we tried to set it at + * the same time. Let's try to set the brightness the system was + * setting to make sure we're in the correct state. */ + if (priv->level.target != level) { + priv->pending = TRUE; + + PHOSH_BACKLIGHT_GET_CLASS (self)->set_level (self, + priv->level.target, + priv->cancel, + on_level_set, + NULL); + } +} + + +void +phosh_backlight_set_brightness (PhoshBacklight *self, double brightness) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + double new_brightness; + int new_level; + + g_return_if_fail (PHOSH_IS_BACKLIGHT (self)); + + new_brightness = CLAMP (brightness, priv->brightness.min, priv->brightness.max); + if (!G_APPROX_VALUE (brightness, new_brightness, FLT_EPSILON)) + g_warning ("Trying to set out-of-range brightness %.2f on %s", brightness, priv->name); + + new_level = phosh_backlight_brightness_to_level (self, brightness); + if (priv->level.target == new_level) + return; + + g_debug ("Setting target brightness to %d", new_level); + priv->brightness.target = new_brightness; + priv->level.target = new_level; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BRIGHTNESS]); + + if (!priv->pending) { + priv->pending = TRUE; + + PHOSH_BACKLIGHT_GET_CLASS (self)->set_level (self, + priv->level.target, + priv->cancel, + on_level_set, + NULL); + } +} + +/** + * phosh_backlight_set_relative: + * @self: The backlight + * @val: The relative brightness + * + * The relative brightness value between `[0.0, 1.0]` is applied + * linearly between the backlight's min and max brightness. + */ +void +phosh_backlight_set_relative (PhoshBacklight *self, double val) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + double brightness; + + g_return_if_fail (PHOSH_IS_BACKLIGHT (self)); + g_return_if_fail (val >= 0.0 && val <= 1.0); + + brightness = priv->brightness.min + ((priv->brightness.max - priv->brightness.min) * val); + phosh_backlight_set_brightness (self, brightness); +} + +/** + * phosh_backlight_get_relative: + * @self: The backlight + * @val: The relative brightness + * + * Get the relative brightness value between `[0.0, 1.0]`. + */ +double +phosh_backlight_get_relative (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + return 1.0 * (priv->brightness.target - priv->brightness.min) / + (priv->brightness.max - priv->brightness.min); +} + + +const char * +phosh_backlight_get_name (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_BACKLIGHT (self), NULL); + + return priv->name; +} + + +void +phosh_backlight_set_name (PhoshBacklight *self, const char *name) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_set_str (&priv->name, name); +} + + +PhoshBacklightScale +phosh_backlight_get_scale (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_BACKLIGHT (self), PHOSH_BACKLIGHT_SCALE_UNKNOWN); + + return priv->scale; +} + + +int +phosh_backlight_get_levels (PhoshBacklight *self) +{ + PhoshBacklightPrivate *priv = phosh_backlight_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_BACKLIGHT (self), 1); + + return 1 + priv->level.max - priv->level.min; +} diff --git a/src/backlight.h b/src/backlight.h new file mode 100644 index 000000000..081d7d5ac --- /dev/null +++ b/src/backlight.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +typedef enum { + PHOSH_BACKLIGHT_SCALE_UNKNOWN = 0, + PHOSH_BACKLIGHT_SCALE_LINEAR = 1, + PHOSH_BACKLIGHT_SCALE_NON_LINEAR = 2, +} PhoshBacklightScale; + +#define PHOSH_TYPE_BACKLIGHT (phosh_backlight_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshBacklight, phosh_backlight, PHOSH, BACKLIGHT, GObject) + +int phosh_backlight_get_brightness (PhoshBacklight *self); +void phosh_backlight_set_brightness (PhoshBacklight *self, double brightness); +double phosh_backlight_get_relative (PhoshBacklight *self); +void phosh_backlight_set_relative (PhoshBacklight *self, double val); +void phosh_backlight_get_range (PhoshBacklight *self, + int *min_brightness, + int *max_brightness); +const char * phosh_backlight_get_name (PhoshBacklight *self); +int phosh_backlight_get_levels (PhoshBacklight *self); +PhoshBacklightScale phosh_backlight_get_scale (PhoshBacklight *self); + +G_END_DECLS diff --git a/src/battery-info.c b/src/battery-info.c new file mode 100644 index 000000000..511dad1e9 --- /dev/null +++ b/src/battery-info.c @@ -0,0 +1,227 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +/* Battery Info widget */ + +#define G_LOG_DOMAIN "phosh-battery-info" + +#include "phosh-config.h" + +#include "battery-info.h" +#include "shell-priv.h" + +/** + * PhoshBatteryInfo: + * + * A widget to display the battery status + */ + +enum { + PROP_0, + PROP_SHOW_DETAIL, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +typedef struct _PhoshBatteryInfo { + PhoshStatusIcon parent; + gboolean present; + gboolean show_detail; +} PhoshBatteryInfo; + + +G_DEFINE_TYPE (PhoshBatteryInfo, phosh_battery_info, PHOSH_TYPE_STATUS_ICON) + + +static void +phosh_battery_info_set_present (PhoshBatteryInfo *self, gboolean present) +{ + g_return_if_fail (PHOSH_IS_BATTERY_INFO (self)); + + if (self->present == present) + return; + + self->present = !!present; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_battery_info_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBatteryInfo *self = PHOSH_BATTERY_INFO (object); + + switch (property_id) { + case PROP_SHOW_DETAIL: + phosh_battery_info_set_show_detail (self, g_value_get_boolean (value)); + break; + case PROP_PRESENT: + phosh_battery_info_set_present (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_battery_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBatteryInfo *self = PHOSH_BATTERY_INFO (object); + + switch (property_id) { + case PROP_SHOW_DETAIL: + g_value_set_boolean (value, self->show_detail); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + + +static void +phosh_battery_info_class_init (PhoshBatteryInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_battery_info_get_property; + object_class->set_property = phosh_battery_info_set_property; + + gtk_widget_class_set_css_name (widget_class, "phosh-battery-info"); + + /** + * PhoshBatteryInfo:show-detail + * + * Whether to show battery percentage detail + */ + props[PROP_SHOW_DETAIL] = + g_param_spec_boolean ("show-detail", "", "", + FALSE, + G_PARAM_CONSTRUCT | + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + /** + * PhoshBatteryInfo:present + * + * Whether battery information is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static gboolean +transform_percent_to_info (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + uint percent = g_value_get_uint (from_value); + char *info; + + info = g_strdup_printf ("%u%%", percent); + g_value_take_string (to_value, info); + return TRUE; +} + + +static void +phosh_battery_info_init (PhoshBatteryInfo *self) +{ + GtkWidget *percentage_label = gtk_label_new (NULL); + PhoshShell *shell = phosh_shell_get_default (); + PhoshBatteryManager *battery_manager = phosh_shell_get_battery_manager (shell); + + phosh_status_icon_set_extra_widget (PHOSH_STATUS_ICON (self), percentage_label); + g_object_bind_property (self, + "show-detail", + percentage_label, + "visible", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (self, + "info", + phosh_status_icon_get_extra_widget (PHOSH_STATUS_ICON (self)), + "label", + G_BINDING_SYNC_CREATE); + + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), "0%"); + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), "battery-missing-symbolic"); + + g_object_bind_property (battery_manager, + "present", + self, + "present", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (battery_manager, + "icon-name", + self, + "icon-name", + G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (battery_manager, + "percent", + self, + "info", + G_BINDING_SYNC_CREATE, + transform_percent_to_info, + NULL, + NULL, + NULL); +} + + +GtkWidget * +phosh_battery_info_new (void) +{ + return g_object_new (PHOSH_TYPE_BATTERY_INFO, NULL); +} + + +void +phosh_battery_info_set_show_detail (PhoshBatteryInfo *self, gboolean show) +{ + g_return_if_fail (PHOSH_IS_BATTERY_INFO (self)); + + if (self->show_detail == show) + return; + + self->show_detail = !!show; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_DETAIL]); +} + + +gboolean +phosh_battery_info_get_show_detail (PhoshBatteryInfo *self) +{ + g_return_val_if_fail (PHOSH_IS_BATTERY_INFO (self), FALSE); + + return self->show_detail; +} diff --git a/src/battery-info.h b/src/battery-info.h new file mode 100644 index 000000000..6aef37bbe --- /dev/null +++ b/src/battery-info.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "status-icon.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BATTERY_INFO (phosh_battery_info_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshBatteryInfo, phosh_battery_info, PHOSH, BATTERY_INFO, PhoshStatusIcon) + +GtkWidget * phosh_battery_info_new (void); +void phosh_battery_info_set_show_detail (PhoshBatteryInfo *self, gboolean show); +gboolean phosh_battery_info_get_show_detail (PhoshBatteryInfo *self); + +G_END_DECLS diff --git a/src/battery-manager.c b/src/battery-manager.c new file mode 100644 index 000000000..b2d3a036b --- /dev/null +++ b/src/battery-manager.c @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-battery-manager" + +#include "phosh-config.h" +#include "battery-manager.h" + +#include "upower.h" + +#include + +/** + * PhoshBatteryManager: + * + * Track batteries and their charging state + */ + +enum { + PROP_0, + PROP_PRESENT, + PROP_ICON_NAME, + PROP_PERCENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBatteryManager { + PhoshManager parent; + + UpClient *upower; + UpDevice *device; + GCancellable *cancel; + + gboolean present; + char *icon_name; + uint percent; +}; +G_DEFINE_TYPE (PhoshBatteryManager, phosh_battery_manager, PHOSH_TYPE_MANAGER) + + +static void +on_property_changed (PhoshBatteryManager *self, + GParamSpec *pspec, + UpDevice *device) +{ + UpDeviceState state; + double percentage; + int smallest_ten; + uint percent; + gboolean is_charging; + gboolean is_charged; + g_autofree char *icon_name = NULL; + + g_object_get (device, "state", &state, "percentage", &percentage, NULL); + + is_charging = state == UP_DEVICE_STATE_CHARGING; + smallest_ten = floor (percentage / 10.0) * 10; + is_charged = state == UP_DEVICE_STATE_FULLY_CHARGED || (is_charging && smallest_ten == 100); + + if (is_charged) { + icon_name = g_strdup ("battery-level-100-charged-symbolic"); + } else { + if (is_charging) + icon_name = g_strdup_printf ("battery-level-%d-charging-symbolic", smallest_ten); + else + icon_name = g_strdup_printf ("battery-level-%d-symbolic", smallest_ten); + } + + if (g_strcmp0 (self->icon_name, icon_name)) { + g_free (self->icon_name); + self->icon_name = g_steal_pointer (&icon_name); + g_debug ("New icon: %s", self->icon_name); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + } + + percent = (int) (percentage + 0.5); + if (self->percent != percent) { + self->percent = percent; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PERCENT]); + } +} + + +static void +on_up_client_new_ready (GObject *source, GAsyncResult *result, gpointer data) +{ + PhoshBatteryManager *self = PHOSH_BATTERY_MANAGER (data); + g_autoptr (GError) err = NULL; + UpClient *upower = NULL; + + upower = up_client_new_finish (result, &err); + if (!upower) { + g_message ("Failed to get UPower Client: %s", err->message); + return; + } + + self->upower = upower; + + /* TODO: this is a oversimplified sync call */ + self->device = up_client_get_display_device (self->upower); + if (self->device == NULL) { + g_warning ("Failed to get upowerd display device"); + return; + } + + g_debug ("Got upower display device"); + g_object_connect (self->device, + "swapped-object-signal::notify::percentage", + G_CALLBACK (on_property_changed), + self, + "swapped-object-signal::notify::state", + G_CALLBACK (on_property_changed), + self, + NULL); + + self->present = TRUE; + on_property_changed (self, NULL, self->device); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_battery_manager_idle_init (PhoshManager *manager) +{ + PhoshBatteryManager *self = PHOSH_BATTERY_MANAGER (manager); + + up_client_new_async (self->cancel, on_up_client_new_ready, self); +} + + +static void +phosh_battery_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBatteryManager *self = PHOSH_BATTERY_MANAGER (object); + + switch (property_id) { + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_PERCENT: + g_value_set_uint (value, self->percent); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_battery_manager_dispose (GObject *object) +{ + PhoshBatteryManager *self = PHOSH_BATTERY_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->device); + g_clear_object (&self->upower); + + g_clear_pointer (&self->icon_name, g_free); + + G_OBJECT_CLASS (phosh_battery_manager_parent_class)->dispose (object); +} + + +static void +phosh_battery_manager_class_init (PhoshBatteryManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->get_property = phosh_battery_manager_get_property; + object_class->dispose = phosh_battery_manager_dispose; + + manager_class->idle_init = phosh_battery_manager_idle_init; + + /** + * PhoshBatteryManager:present + * + * Whether battery information is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBatteryManager:icon-name: + * + * The battery indicator icon name + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBatteryManager:percent: + * + * The charge percentage of the main battery + */ + props[PROP_PERCENT] = + g_param_spec_uint ("percent", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_battery_manager_init (PhoshBatteryManager *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshBatteryManager * +phosh_battery_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_BATTERY_MANAGER, NULL); +} diff --git a/src/battery-manager.h b/src/battery-manager.h new file mode 100644 index 000000000..d45e41080 --- /dev/null +++ b/src/battery-manager.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BATTERY_MANAGER (phosh_battery_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBatteryManager, phosh_battery_manager, PHOSH, BATTERY_MANAGER, PhoshManager) + +PhoshBatteryManager *phosh_battery_manager_new (void); + +G_END_DECLS diff --git a/src/bidi.c b/src/bidi.c new file mode 100644 index 000000000..9f0193d19 --- /dev/null +++ b/src/bidi.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + * + * Based on from which is LGPL-2.1+. + */ + +/* GDK - The GIMP Drawing Kit + * Copyright (C) 1995-1997 Peter Mattis, Spencer Kimball and Josh MacDonald + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +/* + * Modified by the GTK+ Team and others 1997-2000. See the AUTHORS + * file for a list of people on the GTK+ Team. See the ChangeLog + * files for a list of changes. These files are distributed with + * GTK+ at ftp://ftp.gtk.org/pub/gtk/.. + */ + +/* Bits taken from GTK 4.0.2 and tweaked to be used by libhandy. */ + +#include "bidi.h" + +#include + +static PangoDirection +phosh_unichar_direction (gunichar ch) +{ + FriBidiCharType fribidi_ch_type; + + G_STATIC_ASSERT (sizeof (FriBidiChar) == sizeof (gunichar)); + + fribidi_ch_type = fribidi_get_bidi_type (ch); + + if (!FRIBIDI_IS_STRONG (fribidi_ch_type)) + return PANGO_DIRECTION_NEUTRAL; + else if (FRIBIDI_IS_RTL (fribidi_ch_type)) + return PANGO_DIRECTION_RTL; + else + return PANGO_DIRECTION_LTR; +} + +PangoDirection +phosh_find_base_dir (const gchar *text, + gint length) +{ + PangoDirection dir = PANGO_DIRECTION_NEUTRAL; + const gchar *p; + + g_return_val_if_fail (text != NULL || length == 0, PANGO_DIRECTION_NEUTRAL); + + p = text; + while ((length < 0 || p < text + length) && *p) { + gunichar wc = g_utf8_get_char (p); + + dir = phosh_unichar_direction (wc); + + if (dir != PANGO_DIRECTION_NEUTRAL) + break; + + p = g_utf8_next_char (p); + } + + return dir; +} diff --git a/src/bidi.h b/src/bidi.h new file mode 100644 index 000000000..c91bbbdf4 --- /dev/null +++ b/src/bidi.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +G_BEGIN_DECLS + +PangoDirection phosh_find_base_dir (const gchar *text, + gint length); + +G_END_DECLS diff --git a/src/brightness-manager.c b/src/brightness-manager.c new file mode 100644 index 000000000..766596008 --- /dev/null +++ b/src/brightness-manager.c @@ -0,0 +1,897 @@ +/* + * Copyright (C) 2025-2026 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-brightness-manager" + +#include "phosh-config.h" + +#include "auto-brightness.h" +#include "auto-brightness-bucket.h" +#include "brightness-manager.h" +#include "shell-priv.h" +#include "util.h" + +#define KEYBINDINGS_SCHEMA_ID "org.gnome.shell.keybindings" +#define KEYBINDING_KEY_BRIGHTNESS_UP "screen-brightness-up" +#define KEYBINDING_KEY_BRIGHTNESS_DOWN "screen-brightness-down" +#define KEYBINDING_KEY_BRIGHTNESS_UP_MONITOR "screen-brightness-up-monitor" +#define KEYBINDING_KEY_BRIGHTNESS_DOWN_MONITOR "screen-brightness-down-monitor" + +#define POWER_SCHEMA_ID "org.gnome.settings-daemon.plugins.power" + +#define BRIGHTNESS_SCHEMA_ID "mobi.phosh.shell.brightness" +#define BRIGHTNESS_KEY_AUTO_BRIGHTNESS_OFFSET "auto-brightness-offset" + +/** + * PhoshBrightnessManager: + * + * Manage backglight brightness. Handle auto-brightness and maintain a + * `GtkAdjustment` that can be used for brightness sliders. + * + * For auto brightness the `PhoshBrightnessManager` gets the ambient + * brightness from the `PhoshAmbient` manager and feeds these values + * to a `PhoshAutoBrightness` tracker that calculates the resulting + * backlight brightness. Based on other inputs like the currently + * applied offset as set by the user the `PhoshBrightnessManager` + * then sets the actual brightness on the backlight. + */ + +enum { + PROP_0, + PROP_AUTO_BRIGHTNESS_ENABLED, + PROP_ICON_NAME, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +#define MAX_KEYBOARD_LEVELS 20 + +struct _PhoshBrightnessManager { + PhoshDBusBrightnessSkeleton parent; + + GStrv action_names; + GSettings *settings; + PhoshBacklight *backlight; + GtkAdjustment *adjustment; + gulong value_changed_id; + gboolean setting_brightness; + + GSettings *settings_power; + GSettings *settings_brightness; + gboolean dimmed; + struct { + gboolean enabled; + PhoshAutoBrightness *tracker; + double base; + double offset; + guint night_light_temp; + gboolean can_night_light; + } auto_brightness; + + struct { + double target; + double start; + double interval; + double duration; + double elapsed; + uint id; + } transition; + + const char *icon_name; + int dbus_name_id; + double saved_brightness; +}; + +static void phosh_brightness_manager_brightness_init (PhoshDBusBrightnessIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshBrightnessManager, + phosh_brightness_manager, + PHOSH_DBUS_TYPE_BRIGHTNESS_SKELETON, + G_IMPLEMENT_INTERFACE (PHOSH_DBUS_TYPE_BRIGHTNESS, + phosh_brightness_manager_brightness_init)) + +/* https://en.wikipedia.org/wiki/Smoothstep */ +static double +smoothstep (float t) +{ + return t * t * (3.0 - 2.0 * t); +} + + +static gboolean +on_transition_step (gpointer user_data) +{ + PhoshBrightnessManager *self = user_data; + double next, current, smooth; + + self->transition.elapsed += self->transition.interval; + current = phosh_backlight_get_relative (self->backlight); + smooth = smoothstep (CLAMP (self->transition.elapsed / self->transition.duration, 0.0, 1.0)); + next = self->transition.start + (self->transition.target - self->transition.start) * smooth; + + if (!self->auto_brightness.enabled) { + g_debug ("Brightness transition aborted"); + goto end; + } + + if (self->transition.elapsed >= self->transition.duration) { + g_debug ("Brightness transition done at %f, target: %f", next, self->transition.target); + phosh_backlight_set_relative (self->backlight, next); + goto end; + } + + g_debug ("Brightness transition step: current %.3f, next %.3f, target: %.3f", + current, next, self->transition.target); + phosh_backlight_set_relative (self->backlight, next); + return G_SOURCE_CONTINUE; + + end: + self->transition.id = 0; + return G_SOURCE_REMOVE; +} + +/* Human eye adapts faster to higher brightness values */ +#define AUTO_UP_INTERVAL 150 /* ms */ +#define AUTO_DOWN_INTERVAL 400 /* ms */ +#define AUTO_MAX_DURATION 4000 /* ms */ +#define AUTO_STEP_CHANGE 0.025 + +static void +transition_to_brightness (PhoshBrightnessManager *self, double target) +{ + double current = phosh_backlight_get_relative (self->backlight); + uint steps; + + g_clear_handle_id (&self->transition.id, g_source_remove); + + self->transition.target = target; + if (G_APPROX_VALUE (current, self->transition.target, FLT_EPSILON)) + return; + + self->transition.interval = target > current ? AUTO_UP_INTERVAL : AUTO_DOWN_INTERVAL; + + self->transition.elapsed = 0; + self->transition.start = current; + steps = ceil (ABS (self->transition.target - self->transition.start) / 0.025); + if (steps * self->transition.interval > AUTO_MAX_DURATION) { + g_debug ("Limiting max transition duration from %.0fms to %dms", + steps * self->transition.interval, AUTO_MAX_DURATION); + steps = ceil (AUTO_MAX_DURATION / self->transition.interval); + } + self->transition.duration = steps * self->transition.interval; + + g_debug ("Starting auto brightness transition from %.2f to %.2f, duration: %.2fms", + self->transition.start, self->transition.target, self->transition.duration); + + self->transition.id = g_timeout_add (self->transition.interval, on_transition_step, self); +} + + +static double +compensate_night_light (PhoshBrightnessManager *self) +{ + guint temp = self->auto_brightness.night_light_temp; + typedef struct { + guint32 color_temp; /* K */ + double correction; + } ColorCorrection; + + const ColorCorrection corrections[] = { + { 2000, 1.90 }, + { 2250, 1.80 }, + { 2500, 1.70 }, + { 2750, 1.60 }, + { 3000, 1.50 }, + { 3250, 1.40 }, + { 3500, 1.30 }, + { 4000, 1.20 }, + { 5000, 1.10 }, + { 6500, 1.00 }, + }; + + if (!self->auto_brightness.can_night_light) + return 1.0; + + for (int i = 0; i < G_N_ELEMENTS (corrections); i++) { + const ColorCorrection *correction = &corrections[i]; + + if (temp < correction->color_temp) + return correction->correction; + } + + return 1.0; +} + + +static double +calc_auto_brightness (PhoshBrightnessManager *self) +{ + double new_brightness = self->auto_brightness.base; + double night_light_correction = compensate_night_light (self); + + /* Compensate for night light */ + new_brightness *= night_light_correction; + /* Apply any offset the user has set */ + new_brightness += self->auto_brightness.offset; + new_brightness = CLAMP (new_brightness, 0.0, 1.0); + + g_debug ("New auto brightness %.2f (base: %.2f, offset: %.2f, nightlight: %.2f)", + new_brightness, + self->auto_brightness.base, + self->auto_brightness.offset, + night_light_correction); + + return new_brightness; +} + + +static void +on_auto_brightness_changed (PhoshBrightnessManager *self) +{ + double new_brightness; + + g_return_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self)); + + if (!self->backlight) + return; + + if (!self->auto_brightness.enabled) + return; + + new_brightness = phosh_auto_brightness_get_brightness (self->auto_brightness.tracker); + /* TODO: clamp to 100% as we don't do brightness boosts yet */ + self->auto_brightness.base = CLAMP (new_brightness, 0.0, 1.0); + new_brightness = calc_auto_brightness (self); + + transition_to_brightness (self, new_brightness); +} + + +static void +on_night_light_temp_changed (PhoshBrightnessManager *self, + GParamSpec *pspec, + PhoshMonitorManager *monitor_manager) +{ + guint temp; + + g_return_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (monitor_manager)); + + temp = phosh_monitor_manager_get_night_light_temp (monitor_manager); + if (self->auto_brightness.night_light_temp == temp) + return; + self->auto_brightness.night_light_temp = temp; + + if (!self->auto_brightness.enabled) + return; + + g_debug ("Night light temp changed, getting new offset"); + on_auto_brightness_changed (self); +} + + +static void +set_auto_brightness_tracker (PhoshBrightnessManager *self) +{ + if (self->auto_brightness.tracker) + return; + + /* TODO: allow for different brightness trackers */ + self->auto_brightness.tracker = PHOSH_AUTO_BRIGHTNESS (phosh_auto_brightness_bucket_new ()); + g_signal_connect_swapped (self->auto_brightness.tracker, + "notify::brightness", + G_CALLBACK (on_auto_brightness_changed), + self); +} + + +static void +on_ambient_auto_brightness_changed (PhoshBrightnessManager *self, + GParamSpec *pspec, + PhoshAmbient *ambient) +{ + gboolean enabled = phosh_ambient_get_auto_brightness (ambient); + double value; + + g_debug ("Ambient auto-brightness enabled: %d", enabled); + + if (self->auto_brightness.enabled == enabled) + return; + + self->auto_brightness.enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AUTO_BRIGHTNESS_ENABLED]); + + self->icon_name = enabled ? "auto-brightness-symbolic" : "display-brightness-symbolic"; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + + if (self->auto_brightness.enabled) { + self->auto_brightness.offset = g_settings_get_double (self->settings_brightness, + BRIGHTNESS_KEY_AUTO_BRIGHTNESS_OFFSET); + /* Adjustment is [0.0, 1.0] */ + value = self->auto_brightness.offset + 0.5; + set_auto_brightness_tracker (self); + on_auto_brightness_changed (self); + } else { + value = phosh_backlight_get_relative (self->backlight); + } + + gtk_adjustment_set_value (self->adjustment, value); +} + + +static void +on_ambient_light_level_changed (PhoshBrightnessManager *self, + GParamSpec *pspec, + PhoshAmbient *ambient) +{ + double level; + + if (!self->auto_brightness.enabled) + return; + + level = phosh_ambient_get_light_level (ambient); + g_debug ("Ambient light level: %.2f lux", level); + + phosh_auto_brightness_add_ambient_level (self->auto_brightness.tracker, level); +} + + +static void +on_name_acquired (GDBusConnection *connection, const char *name, gpointer user_data) +{ + g_debug ("Acquired name %s", name); +} + + +static void +on_name_lost (GDBusConnection *connection, const char *name, gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, const char *name, gpointer user_data) +{ + g_autoptr (GError) err = NULL; + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (user_data); + + /* We need to use GNOME Shell's object path here to make g-s-d happy */ + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/gnome/Shell/Brightness", + &err)) { + g_warning ("Failed export brightness interface: %s", err->message); + } +} + + +static gboolean +phosh_brightness_manager_handle_set_auto_brightness_target (PhoshDBusBrightness *object, + GDBusMethodInvocation *invocation, + gdouble arg_target) +{ + g_debug ("Target brightness: %f", arg_target); + + /* Nothing to do here, we handle it internally */ + /* https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/merge_requests/442 */ + phosh_dbus_brightness_complete_set_auto_brightness_target (object, invocation); + return TRUE; +} + + +static gboolean +phosh_brightness_manager_handle_set_dimming (PhoshDBusBrightness *object, + GDBusMethodInvocation *invocation, + gboolean arg_enable) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (object); + double target; + + g_debug ("Dimming: %s", arg_enable ? "enabled" : "disabled"); + + if (!self->backlight) { + g_dbus_method_invocation_return_error (invocation, + G_DBUS_ERROR, + G_DBUS_ERROR_FILE_NOT_FOUND, + "No backlight"); + return TRUE; + } + + if (arg_enable) { + double current = phosh_backlight_get_relative (self->backlight); + + target = g_settings_get_int (self->settings_power, "idle-brightness") * 0.01; + + /* If current brightness is lower than dim brightness don't do anything */ + if (target >= current) { + self->saved_brightness = -1.0; + goto done; + } + + self->saved_brightness = current; + } else { + target = self->saved_brightness; + self->saved_brightness = -1.0; + } + + if (target >= 0.0) + phosh_backlight_set_relative (self->backlight, target); + + done: + phosh_dbus_brightness_complete_set_dimming (object, invocation); + return TRUE; +} + + +static gboolean +phosh_brightness_manager_handle_get_has_brightness_control (PhoshDBusBrightness *object) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (object); + + return !!self->backlight; +} + + +static void +phosh_brightness_manager_brightness_init (PhoshDBusBrightnessIface *iface) +{ + iface->handle_set_auto_brightness_target = phosh_brightness_manager_handle_set_auto_brightness_target; + iface->handle_set_dimming = phosh_brightness_manager_handle_set_dimming; + iface->get_has_brightness_control = phosh_brightness_manager_handle_get_has_brightness_control; +} + + +static void +on_backlight_brightness_changed (PhoshBrightnessManager *self, + GParamSpec *pspec, + PhoshBacklight *backlight) +{ + double value; + + g_assert (self->backlight == backlight); + + /* With auto brightness the slider gives an offset to the auto brightness target */ + if (self->auto_brightness.enabled) + return; + + if (self->setting_brightness) + return; + + value = phosh_backlight_get_relative (self->backlight); + + g_signal_handler_block (self->adjustment, self->value_changed_id); + gtk_adjustment_set_value (self->adjustment, value); + g_signal_handler_unblock (self->adjustment, self->value_changed_id); +} + + +static void +on_value_changed (PhoshBrightnessManager *self, GtkAdjustment *adjustment) +{ + double value, new_brightness; + + g_assert (self->adjustment == adjustment); + + if (!self->backlight) + return; + + value = gtk_adjustment_get_value (self->adjustment); + + /* With auto brightness the slider gives an offset to the auto brightness target */ + if (self->auto_brightness.enabled) { + /* TODO: should we go through the brightness curve? */ + /* Auto-brightness offset is [-0.5, +0.5] */ + double offset = CLAMP (value - 0.5, -0.5, 0.5); + + if (G_APPROX_VALUE (offset, self->auto_brightness.offset, FLT_EPSILON)) + return; + self->auto_brightness.offset = offset; + + new_brightness = calc_auto_brightness (self); + /* Cancel any ongoing transition, the user likely wants the new brightness right away */ + g_clear_handle_id (&self->transition.id, g_source_remove); + + g_debug ("Updating auto-brightness offset %f", offset); + g_settings_set_double (self->settings_brightness, + BRIGHTNESS_KEY_AUTO_BRIGHTNESS_OFFSET, + offset); + } else { + new_brightness = value; + } + + self->setting_brightness = TRUE; + phosh_backlight_set_relative (self->backlight, new_brightness); + self->setting_brightness = FALSE; +} + + +static void +set_backlight (PhoshBrightnessManager *self, PhoshBacklight *backlight) +{ + if (self->backlight == backlight) + return; + + if (self->backlight) + g_signal_handlers_disconnect_by_data (self->backlight, self); + + g_set_object (&self->backlight, backlight); + self->saved_brightness = -1.0; + + if (self->backlight) { + g_debug ("Found %s for brightness control", phosh_backlight_get_name (self->backlight)); + + g_signal_connect_swapped (self->backlight, + "notify::brightness", + G_CALLBACK (on_backlight_brightness_changed), + self); + if (self->auto_brightness.enabled) + on_auto_brightness_changed (self); + else + on_backlight_brightness_changed (self, NULL, self->backlight); + } +} + + +static void +on_primary_monitor_changed (PhoshBrightnessManager *self, GParamSpec *psepc, PhoshShell *shell) +{ + PhoshMonitor *monitor = phosh_shell_get_primary_monitor (shell); + PhoshBacklight *backlight = NULL; + + if (monitor && monitor->backlight) + backlight = monitor->backlight; + + /* Fall back to built in display */ + if (!backlight) + monitor = phosh_shell_get_builtin_monitor (shell); + + if (monitor) { + backlight = monitor->backlight; + self->auto_brightness.can_night_light = phosh_monitor_has_gamma (monitor); + } + + set_backlight (self, backlight); + phosh_dbus_brightness_set_has_brightness_control (PHOSH_DBUS_BRIGHTNESS (self), + !!self->backlight); +} + + +static void +on_auto_brightness_offset_changed (PhoshBrightnessManager *self, + const char *key, + GSettings *settings) +{ + double offset = g_settings_get_double (settings, BRIGHTNESS_KEY_AUTO_BRIGHTNESS_OFFSET); + + /* If auto brightness is disabled we'll pick up the offset when it gets enabled */ + if (!self->auto_brightness.enabled) + return; + + /* Adjustment goes from [0.0, 1.0] */ + offset += 0.5; + phosh_brightness_manager_set_value (self, offset, FALSE); +} + + +static void +show_osd (PhoshBrightnessManager *self, double brightness) +{ + PhoshShell *shell = phosh_shell_get_default (); + + if (phosh_shell_get_state (shell) & PHOSH_STATE_SETTINGS) + return; + + phosh_shell_show_osd (shell, + NULL, + self->icon_name, + NULL, + 100.0 * brightness, + 100.0); +} + + +static void +adjust_brightness (PhoshBrightnessManager *self, gboolean up) +{ + int levels; + double brightness, step; + + if (!self->backlight) + return; + + levels = phosh_backlight_get_levels (self->backlight); + levels = MIN (MAX_KEYBOARD_LEVELS, levels); + step = 1.0 / levels; + brightness = phosh_backlight_get_relative (self->backlight); + + if (up) + brightness += step; + else + brightness -= step; + + brightness = CLAMP (brightness, 0.0, 1.0); + phosh_backlight_set_relative (self->backlight, brightness); + + show_osd (self, brightness); +} + + +static void +on_brightness_up (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (data); + + adjust_brightness (self, TRUE); +} + + +static void +on_brightness_down (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (data); + + adjust_brightness (self, FALSE); +} + + +static void +add_keybindings (PhoshBrightnessManager *self) +{ + g_autoptr (GArray) actions = g_array_new (FALSE, TRUE, sizeof (GActionEntry)); + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_BRIGHTNESS_UP, + on_brightness_up); + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_BRIGHTNESS_DOWN, + on_brightness_down); + /* TODO: use current monitor */ + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_BRIGHTNESS_UP_MONITOR, + on_brightness_up); + /* TODO: use current monitor */ + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_BRIGHTNESS_DOWN_MONITOR, + on_brightness_down); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry *)actions->data, + actions->len, + self); + self->action_names = g_strv_builder_end (builder); +} + + +static void +on_keybindings_changed (PhoshBrightnessManager *self) +{ + g_debug ("Updating keybindings in BrightnessManager"); + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + add_keybindings (self); +} + + +static void +phosh_brightness_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (object); + + switch (property_id) { + case PROP_AUTO_BRIGHTNESS_ENABLED: + g_value_set_boolean (value, self->auto_brightness.enabled); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_brightness_manager_dispose (GObject *object) +{ + PhoshBrightnessManager *self = PHOSH_BRIGHTNESS_MANAGER (object); + + g_clear_handle_id (&self->transition.id, g_source_remove); + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + set_backlight (self, NULL); + g_clear_pointer (&self->action_names, g_strfreev); + g_clear_object (&self->settings); + g_clear_object (&self->settings_power); + g_clear_object (&self->settings_brightness); + g_clear_signal_handler (&self->value_changed_id, self->adjustment); + g_clear_object (&self->adjustment); + + g_clear_object (&self->auto_brightness.tracker); + + G_OBJECT_CLASS (phosh_brightness_manager_parent_class)->dispose (object); +} + + +static void +phosh_brightness_manager_class_init (PhoshBrightnessManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_brightness_manager_dispose; + object_class->get_property = phosh_brightness_manager_get_property; + + /** + * PhoshBrightnessManager:auto-brightness-enabled: + * + * If `TRUE` the display brightness is currently being adjusted to + * ambient light levels + */ + props[PROP_AUTO_BRIGHTNESS_ENABLED] = + g_param_spec_boolean ("auto-brightness-enabled", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBrightnessManager:icon-name: + * + * An icon suitable for display in a brightness slider + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + + +static void +phosh_brightness_manager_init (PhoshBrightnessManager *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshAmbient *ambient = phosh_shell_get_ambient (phosh_shell_get_default ()); + + self->saved_brightness = -1.0; + self->icon_name = "display-brightness-symbolic"; + self->settings_power = g_settings_new (POWER_SCHEMA_ID); + + self->settings_brightness = g_settings_new (BRIGHTNESS_SCHEMA_ID); + g_signal_connect_swapped (self->settings_brightness, + "changed::" BRIGHTNESS_KEY_AUTO_BRIGHTNESS_OFFSET, + G_CALLBACK (on_auto_brightness_offset_changed), + self); + + self->adjustment = g_object_ref_sink (gtk_adjustment_new (0, 0, 1.0, 0.01, 0.01, 0)); + self->value_changed_id = g_signal_connect_swapped (self->adjustment, + "value-changed", + G_CALLBACK (on_value_changed), + self); + + g_signal_connect_object (shell, + "notify::primary-monitor", + G_CALLBACK (on_primary_monitor_changed), + self, + G_CONNECT_SWAPPED); + on_primary_monitor_changed (self, NULL, shell); + + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.gnome.Shell.Brightness", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); + + if (ambient) { + g_object_connect (ambient, + "swapped-object-signal::notify::auto-brightness-enabled", + on_ambient_auto_brightness_changed, + self, + "swapped-object-signal::notify::light-level", + on_ambient_light_level_changed, + self, + NULL); + } + + g_signal_connect_object (phosh_shell_get_monitor_manager (shell), + "notify::night-light-temp", + G_CALLBACK (on_night_light_temp_changed), + self, + G_CONNECT_SWAPPED); + + self->settings = g_settings_new (KEYBINDINGS_SCHEMA_ID); + g_signal_connect_object (self->settings, + "changed", + G_CALLBACK (on_keybindings_changed), + self, + G_CONNECT_SWAPPED); + add_keybindings (self); +} + + +PhoshBrightnessManager * +phosh_brightness_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_BRIGHTNESS_MANAGER, NULL); +} + + +GtkAdjustment * +phosh_brightness_manager_get_adjustment (PhoshBrightnessManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self), NULL); + + return self->adjustment; +} + + +gboolean +phosh_brightness_manager_get_auto_brightness_enabled (PhoshBrightnessManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self), FALSE); + + return self->auto_brightness.enabled; +} + +/** + * phosh_brightness_manager_get_value: + * @PhoshBrightnessManager: The brightness manager + * + * Get the value of the brightness adjustment. The interpretation of the value depends + * on whether auto brightness is enabled or not. + * + * Returns: The current value of the adjustment [0.0, 1.0] + */ +double +phosh_brightness_manager_get_value (PhoshBrightnessManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self), 0.5); + + return gtk_adjustment_get_value (self->adjustment); +} + +/** + * phosh_brightness_manager_set_value: + * @PhoshBrightnessManager: The brightness manager + * @value: The brightness adjustment value [0.0, 1.0]. + * @osd: Whether to show the osd when setting the value + * + * Set the value of the brightness adjustment. The interpretation of the value depends + * on whether auto brightness is enabled or not. + */ +void +phosh_brightness_manager_set_value (PhoshBrightnessManager *self, + double value, + gboolean osd) +{ + g_return_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (self)); + g_return_if_fail (0.0 <= value && value <= 1.0); + + gtk_adjustment_set_value (self->adjustment, value); + if (osd) + show_osd (self, value); +} diff --git a/src/brightness-manager.h b/src/brightness-manager.h new file mode 100644 index 000000000..25a7010ff --- /dev/null +++ b/src/brightness-manager.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "phosh-brightness-dbus.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BRIGHTNESS_MANAGER (phosh_brightness_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBrightnessManager, phosh_brightness_manager, PHOSH, BRIGHTNESS_MANAGER, + PhoshDBusBrightnessSkeleton) + +PhoshBrightnessManager *phosh_brightness_manager_new (void); +GtkAdjustment * phosh_brightness_manager_get_adjustment (PhoshBrightnessManager *self); +gboolean phosh_brightness_manager_get_auto_brightness_enabled (PhoshBrightnessManager *self); +double phosh_brightness_manager_get_value (PhoshBrightnessManager *self); +void phosh_brightness_manager_set_value (PhoshBrightnessManager *self, + double value, + gboolean osd); + + +G_END_DECLS diff --git a/src/brightness-settings.c b/src/brightness-settings.c new file mode 100644 index 000000000..41b61f9a3 --- /dev/null +++ b/src/brightness-settings.c @@ -0,0 +1,146 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-brightness-settings" + +#include "phosh-config.h" + +#include "brightness-settings.h" +#include "shell-priv.h" + +#define POWER_SCHEMA "org.gnome.settings-daemon.plugins.power" +#define KEY_AMBIENT_ENABLED "ambient-enabled" + +/** + * PhoshBrightnessSettings: + * + * A widget to display brightness controls. + */ + +struct _PhoshBrightnessSettings { + GtkBin parent; + + GtkSwitch *auto_switch; + GtkImage *image; + GtkScale *scale; + GtkToggleButton *toggle_btn; + GtkStack *toggle_stack; + + GSettings *settings; +}; + +G_DEFINE_TYPE (PhoshBrightnessSettings, phosh_brightness_settings, GTK_TYPE_BIN); + + +static void +on_auto_brightness_activated (PhoshBrightnessSettings *self, GtkListBoxRow *row) +{ + gtk_switch_set_active (self->auto_switch, !gtk_switch_get_active (self->auto_switch)); +} + + +static void +phosh_brightness_settings_dispose (GObject *object) +{ + PhoshBrightnessSettings *self = PHOSH_BRIGHTNESS_SETTINGS (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_brightness_settings_parent_class)->dispose (object); +} + + +static void +phosh_brightness_settings_class_init (PhoshBrightnessSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_brightness_settings_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/brightness-settings.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshBrightnessSettings, auto_switch); + gtk_widget_class_bind_template_child (widget_class, PhoshBrightnessSettings, image); + gtk_widget_class_bind_template_child (widget_class, PhoshBrightnessSettings, scale); + gtk_widget_class_bind_template_child (widget_class, PhoshBrightnessSettings, toggle_btn); + gtk_widget_class_bind_template_child (widget_class, PhoshBrightnessSettings, toggle_stack); + + gtk_widget_class_bind_template_callback (widget_class, on_auto_brightness_activated); + + gtk_widget_class_set_css_name (widget_class, "phosh-brightness-settings"); +} + + +static gboolean +transform_toggle_to_stack_child_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer data) +{ + gboolean active = g_value_get_boolean (from_value); + + g_value_set_string (to_value, active ? "show-details" : "hide-details"); + + return TRUE; +} + + +static void +phosh_brightness_settings_init (PhoshBrightnessSettings *self) +{ + PhoshShell *shell; + PhoshBrightnessManager *brightness_manager; + GtkAdjustment *adjustment; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->settings = g_settings_new (POWER_SCHEMA); + + shell = phosh_shell_get_default (); + brightness_manager = phosh_shell_get_brightness_manager (shell); + adjustment = phosh_brightness_manager_get_adjustment (brightness_manager); + gtk_range_set_adjustment (GTK_RANGE (self->scale), adjustment); + + g_settings_bind (self->settings, KEY_AMBIENT_ENABLED, self->auto_switch, "active", + G_SETTINGS_BIND_GET | G_SETTINGS_BIND_SET); + + g_object_bind_property (brightness_manager, "icon-name", + self->image, "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (brightness_manager, "auto-brightness-enabled", + self->scale, "has-origin", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + g_object_bind_property (brightness_manager, "has-brightness-control", + self, "visible", + G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self->toggle_btn, "active", + self->toggle_stack, "visible-child-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_toggle_to_stack_child_name, NULL, NULL, NULL); +} + + +GtkWidget * +phosh_brightness_settings_new (void) +{ + return g_object_new (PHOSH_TYPE_BRIGHTNESS_SETTINGS, NULL); +} + + +void +phosh_brightness_settings_hide_details (PhoshBrightnessSettings *self) +{ + g_return_if_fail (PHOSH_IS_BRIGHTNESS_SETTINGS (self)); + + gtk_toggle_button_set_active (self->toggle_btn, FALSE); +} diff --git a/src/brightness-settings.h b/src/brightness-settings.h new file mode 100644 index 000000000..c7ccc1ebb --- /dev/null +++ b/src/brightness-settings.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BRIGHTNESS_SETTINGS phosh_brightness_settings_get_type () + +G_DECLARE_FINAL_TYPE (PhoshBrightnessSettings, phosh_brightness_settings, PHOSH, + BRIGHTNESS_SETTINGS, GtkBin) + +GtkWidget *phosh_brightness_settings_new (void); +void phosh_brightness_settings_hide_details (PhoshBrightnessSettings *self); + +G_END_DECLS diff --git a/src/bt-device-row.c b/src/bt-device-row.c new file mode 100644 index 000000000..9026307cd --- /dev/null +++ b/src/bt-device-row.c @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-device-row" + +#include "bt-device-row.h" +#include "shell-priv.h" + +/** + * PhoshBtDeviceRow: + * + * A widget to display a Bluetooth device + */ + +enum { + PROP_0, + PROP_DEVICE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + ACTIVATING, + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + + +struct _PhoshBtDeviceRow { + HdyActionRow parent; + + GtkWidget *icon; + GtkSpinner *spinner; + GtkRevealer *revealer; + double bat_percentage; + + BluetoothDevice *device; + + GCancellable *cancellable; +}; + +G_DEFINE_TYPE (PhoshBtDeviceRow, phosh_bt_device_row, HDY_TYPE_ACTION_ROW); + + +static void +bat_level_cb (PhoshBtDeviceRow *self, GParamSpec *pspec, BluetoothDevice *device) +{ + double current; + gboolean connected; + BluetoothBatteryType type; + g_autofree char *subtitle = NULL; + + g_assert (PHOSH_IS_BT_DEVICE_ROW (self)); + g_assert (BLUETOOTH_IS_DEVICE (device)); + + g_object_get (device, + "battery-percentage", ¤t, + "battery-type", &type, + "connected", &connected, + NULL); + + if (!connected || type != BLUETOOTH_BATTERY_TYPE_PERCENTAGE) { + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), ""); + self->bat_percentage = -1.0; + return; + } + + current = round (current); + if (G_APPROX_VALUE (round (self->bat_percentage), current, FLT_EPSILON)) + return; + + self->bat_percentage = current; + /* Translators: a battery level in percent */ + subtitle = g_strdup_printf (_("Battery %.0f%%"), self->bat_percentage); + hdy_action_row_set_subtitle (HDY_ACTION_ROW (self), subtitle); +} + + +static void +phosh_bt_device_row_set_device (PhoshBtDeviceRow *self, BluetoothDevice *device) +{ + g_set_object (&self->device, device); + + g_object_bind_property (self->device, + "connectable", + self, + "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "alias", + self, + "title", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "icon", + self->icon, + "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->device, + "connected", + self->revealer, + "reveal-child", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_connect (self->device, + "swapped-object-signal::notify::battery-percentage", bat_level_cb, self, + "swapped-object-signal::notify::battery-type", bat_level_cb, self, + "swapped-object-signal::notify::connected", bat_level_cb, self, + NULL); + bat_level_cb (self, NULL, device); +} + + +static void +phosh_bt_device_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshBtDeviceRow *self = PHOSH_BT_DEVICE_ROW (object); + + switch (property_id) { + case PROP_DEVICE: + phosh_bt_device_row_set_device (self, g_value_dup_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_bt_device_row_dispose (GObject *object) +{ + PhoshBtDeviceRow *self = PHOSH_BT_DEVICE_ROW (object); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + g_clear_object (&self->device); + + G_OBJECT_CLASS (phosh_bt_device_row_parent_class)->dispose (object); +} + + +static void +on_connect_device_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (PhoshBtDeviceRow) self = PHOSH_BT_DEVICE_ROW (user_data); + PhoshBtManager *manager = PHOSH_BT_MANAGER (source_object); + g_autoptr (GError) err = NULL; + gboolean success; + + success = phosh_bt_manager_connect_device_finish (manager, res, &err); + if (!success) + g_warning ("Failed to connect: %s", err->message); + + g_assert (PHOSH_IS_BT_DEVICE_ROW (user_data)); + + gtk_spinner_stop (self->spinner); + + g_signal_emit (self, signals[DONE], 0, success); +} + + +static void +on_bt_row_activated (PhoshBtDeviceRow *self) +{ + PhoshBtManager *manager = phosh_shell_get_bt_manager (phosh_shell_get_default ()); + gboolean connected; + g_autofree char *name = NULL; + + g_object_get (self->device, "connected", &connected, "alias", &name, NULL); + + g_cancellable_cancel (self->cancellable); + g_set_object (&self->cancellable, g_cancellable_new ()); + + g_debug ("%sonnecting device %s", !connected ? "C" : "Disc", name); + + gtk_spinner_start (self->spinner); + phosh_bt_manager_connect_device_async (manager, + self->device, + !connected, + on_connect_device_ready, + self->cancellable, + g_object_ref (self)); + g_signal_emit (self, signals[ACTIVATING], 0); +} + + +static void +phosh_bt_device_row_class_init (PhoshBtDeviceRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_bt_device_row_set_property; + object_class->dispose = phosh_bt_device_row_dispose; + + /** + * PhoshBtDeviceRow:device: + * + * The bluetooth device represented by the row + */ + props[PROP_DEVICE] = + g_param_spec_object ("device", "", "", + BLUETOOTH_TYPE_DEVICE, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/bt-device-row.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, revealer); + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, spinner); + gtk_widget_class_bind_template_child (widget_class, PhoshBtDeviceRow, icon); + + gtk_widget_class_bind_template_callback (widget_class, on_bt_row_activated); + + /** + * PhoshBtDeviceRow::activating + * + * The device is in the process of being activated / deactivated. + */ + signals[ACTIVATING] = g_signal_new ("activating", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + /** + * PhoshBtDeviceRow::done + * @self: The device row + * @success: Whether the device (de)activation was successful + * + * The user selected the row and the activation finished. + */ + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_BOOLEAN); +} + + +static void +phosh_bt_device_row_init (PhoshBtDeviceRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + /* Negative values mean "undefined" */ + self->bat_percentage = -1.0; +} + + +GtkWidget * +phosh_bt_device_row_new (BluetoothDevice *device) +{ + g_assert (BLUETOOTH_IS_DEVICE (device)); + + return GTK_WIDGET (g_object_new (PHOSH_TYPE_BT_DEVICE_ROW, "device", device, NULL)); +} diff --git a/src/bt-device-row.h b/src/bt-device-row.h new file mode 100644 index 000000000..dc965e9cd --- /dev/null +++ b/src/bt-device-row.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "bluetooth-device.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_DEVICE_ROW phosh_bt_device_row_get_type () +G_DECLARE_FINAL_TYPE (PhoshBtDeviceRow, phosh_bt_device_row, PHOSH, BT_DEVICE_ROW, HdyActionRow) + +GtkWidget *phosh_bt_device_row_new (BluetoothDevice *device); + +G_END_DECLS diff --git a/src/bt-info.c b/src/bt-info.c new file mode 100644 index 000000000..11a38a709 --- /dev/null +++ b/src/bt-info.c @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-info" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "bt-info.h" +#include "bt-manager.h" + +#include "gmobile.h" + +/** + * PhoshBtInfo: + * + * A widget to display the bluetooth status + * + * #PhoshBtInfo displays the current bluetooth status based on information + * from #PhoshBtManager. To figure out if the widget should be shown + * the #PhoshBtInfo:enabled property can be useful. + */ + +enum { + PROP_0, + PROP_ENABLED, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBtInfo { + PhoshStatusIcon parent; + + gboolean enabled; + gboolean present; + PhoshBtManager *bt; +}; +G_DEFINE_TYPE (PhoshBtInfo, phosh_bt_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_bt_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBtInfo *self = PHOSH_BT_INFO (object); + + switch (property_id) { + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +update_icon (PhoshBtInfo *self, GParamSpec *pspec, PhoshBtManager *bt) +{ + const char *icon_name; + + g_return_if_fail (PHOSH_IS_BT_INFO (self)); + g_return_if_fail (PHOSH_IS_BT_MANAGER (bt)); + + icon_name = phosh_bt_manager_get_icon_name (bt); + g_debug ("Updating bt icon to %s", icon_name); + if (icon_name) + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), icon_name); +} + + +static void +update_info (PhoshBtInfo *self) +{ + g_autofree char *msg = NULL; + gboolean enabled; + guint n_connected; + + g_return_if_fail (PHOSH_IS_BT_INFO (self)); + + enabled = phosh_bt_manager_get_enabled (self->bt); + if (!enabled) { + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Bluetooth")); + return; + } + + n_connected = phosh_bt_manager_get_n_connected (self->bt); + switch (n_connected) { + case 0: + break; + case 1: { + const char *info = phosh_bt_manager_get_info (self->bt); + if (gm_str_is_null_or_empty (info)) { + /* Translators: One connected Bluetooth device */ + msg = g_strdup_printf ("One device"); + } else { + msg = g_strdup (info); + } + break; + } + default: + /* Translators: The number of currently connected Bluetooth devices */ + msg = g_strdup_printf ("%d devices", n_connected); + } + + if (msg) { + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), msg); + return; + } + + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), C_("bluetooth:enabled", "On")); +} + + +static void +on_bt_enabled (PhoshBtInfo *self, GParamSpec *pspec, PhoshBtManager *bt) +{ + gboolean enabled; + + g_return_if_fail (PHOSH_IS_BT_INFO (self)); + g_return_if_fail (PHOSH_IS_BT_MANAGER (bt)); + + enabled = phosh_bt_manager_get_enabled (bt); + g_debug ("Updating bt enabled %d", enabled); + if (self->enabled == enabled) + return; + + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); + + update_info (self); +} + + +static void +on_bt_present (PhoshBtInfo *self, GParamSpec *pspec, PhoshBtManager *bt) +{ + gboolean present; + + g_return_if_fail (PHOSH_IS_BT_INFO (self)); + g_return_if_fail (PHOSH_IS_BT_MANAGER (bt)); + + present = phosh_bt_manager_get_present (bt); + if (self->present == present) + return; + + self->present = present; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_bt_info_idle_init (PhoshStatusIcon *icon) +{ + PhoshBtInfo *self = PHOSH_BT_INFO (icon); + + update_icon (self, NULL, self->bt); + update_info (self); + on_bt_enabled (self, NULL, self->bt); +} + + +static void +phosh_bt_info_constructed (GObject *object) +{ + PhoshBtInfo *self = PHOSH_BT_INFO (object); + PhoshShell *shell; + + G_OBJECT_CLASS (phosh_bt_info_parent_class)->constructed (object); + + shell = phosh_shell_get_default (); + self->bt = g_object_ref (phosh_shell_get_bt_manager (shell)); + + if (self->bt == NULL) { + g_warning ("Failed to get bt manager"); + return; + } + + g_object_connect (self->bt, + "swapped-signal::notify::icon-name", update_icon, self, + "swapped-signal::notify::enabled", on_bt_enabled, self, + "swapped-signal::notify::present", on_bt_present, self, + "swapped-signal::notify::n-connected", update_info, self, + NULL); +} + + +static void +phosh_bt_info_dispose (GObject *object) +{ + PhoshBtInfo *self = PHOSH_BT_INFO (object); + + if (self->bt) { + g_signal_handlers_disconnect_by_data (self->bt, self); + g_clear_object (&self->bt); + } + + G_OBJECT_CLASS (phosh_bt_info_parent_class)->dispose (object); +} + + +static void +phosh_bt_info_class_init (PhoshBtInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshStatusIconClass *status_icon_class = PHOSH_STATUS_ICON_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_bt_info_constructed; + object_class->dispose = phosh_bt_info_dispose; + object_class->get_property = phosh_bt_info_get_property; + + status_icon_class->idle_init = phosh_bt_info_idle_init; + + gtk_widget_class_set_css_name (widget_class, "phosh-bt-info"); + + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + "enabled", + "Whether a bt device is enabled", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + props[PROP_PRESENT] = + g_param_spec_boolean ("present", + "Present", + "Whether bt hardware is present", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_bt_info_init (PhoshBtInfo *self) +{ +} + + +GtkWidget * +phosh_bt_info_new (void) +{ + return g_object_new (PHOSH_TYPE_BT_INFO, NULL); +} diff --git a/src/bt-info.h b/src/bt-info.h new file mode 100644 index 000000000..3883f7bf8 --- /dev/null +++ b/src/bt-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_INFO (phosh_bt_info_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBtInfo, phosh_bt_info, PHOSH, BT_INFO, PhoshStatusIcon) + +GtkWidget * phosh_bt_info_new (void); + +G_END_DECLS diff --git a/src/bt-manager.c b/src/bt-manager.c new file mode 100644 index 000000000..aa38d2a71 --- /dev/null +++ b/src/bt-manager.c @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-manager" + +#include "phosh-config.h" + +#include "bt-manager.h" +#include "shell-priv.h" +#include "dbus/gsd-rfkill-dbus.h" +#include "util.h" + +#include "gtk-list-models/gtkfilterlistmodel.h" +#include "gnome-bluetooth-enum-types.h" +#include "bluetooth-client.h" +#include "bluetooth-device.h" + +#define BUS_NAME "org.gnome.SettingsDaemon.Rfkill" +#define OBJECT_PATH "/org/gnome/SettingsDaemon/Rfkill" + +/** + * PhoshBtManager: + * + * Tracks the Bluetooth status + * + * #PhoshBtManager tracks the Bluetooth status that + * is whether the adapter is present and enabled. + */ + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_ENABLED, + PROP_PRESENT, + PROP_N_DEVICES, + PROP_N_CONNECTED, + PROP_INFO, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshBtManager { + PhoshManager manager; + + gboolean enabled; + gboolean present; + const char *icon_name; + guint n_connected; + guint n_devices; + char *info; + + BluetoothClient *bt_client; + GtkFilterListModel *connectable_devices; + + PhoshDBusRfkill *proxy; +}; +G_DEFINE_TYPE (PhoshBtManager, phosh_bt_manager, PHOSH_TYPE_MANAGER); + + +static void +on_adapter_setup_mode_changed (PhoshBtManager *self) +{ + gboolean setup_mode; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_CLIENT (self->bt_client)); + + g_object_get (self->bt_client, "default-adapter-setup-mode", &setup_mode, NULL); + + g_debug ("Setup-mode: %d", setup_mode); +} + + +static void +on_adapter_state_changed (PhoshBtManager *self) +{ + BluetoothAdapterState state; + g_autofree char *name = NULL; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_CLIENT (self->bt_client)); + + g_object_get (self->bt_client, "default-adapter-state", &state, NULL); + name = g_enum_to_string (BLUETOOTH_TYPE_ADAPTER_STATE, state); + + g_debug ("State: %s", name); +} + + +static void +phosh_bt_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshBtManager *self = PHOSH_BT_MANAGER (object); + + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_N_DEVICES: + g_value_set_uint (value, self->n_devices); + break; + case PROP_N_CONNECTED: + g_value_set_uint (value, self->n_connected); + break; + case PROP_INFO: + g_value_set_string (value, self->info); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_bt_airplane_mode_changed (PhoshBtManager *self, GParamSpec *pspec, PhoshDBusRfkill *proxy) +{ + gboolean enabled; + const char *icon_name; + + g_return_if_fail (PHOSH_IS_BT_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_RFKILL (proxy)); + + enabled = !phosh_dbus_rfkill_get_bluetooth_airplane_mode (proxy) && self->present; + + if (enabled == self->enabled) + return; + + self->enabled = enabled; + + g_debug ("BT enabled: %d", self->enabled); + if (enabled) + icon_name = "bluetooth-active-symbolic"; + else + icon_name = "bluetooth-disabled-symbolic"; + + if (icon_name != self->icon_name) { + self->icon_name = icon_name; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + + +static void +on_bt_has_airplane_mode_changed (PhoshBtManager *self, GParamSpec *pspec, PhoshDBusRfkill *proxy) +{ + gboolean present; + + present = phosh_dbus_rfkill_get_bluetooth_has_airplane_mode (proxy); + + if (present == self->present) + return; + + /* Having a BT adapter that supports airplane mode seems to be the best + indicator for having a device at all */ + self->present = present; + g_debug ("BT present: %d", self->present); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); + + /* Sync up in case `enabled` got flipped first */ + on_bt_airplane_mode_changed (self, NULL, proxy); +} + + +static void +on_proxy_new_for_bus_finish (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshBtManager *self = PHOSH_BT_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_IS_BT_MANAGER (self)); + + self->proxy = phosh_dbus_rfkill_proxy_new_for_bus_finish (res, &err); + + if (!self->proxy) { + phosh_dbus_service_error_warn (err, "Failed to get gsd rfkill proxy"); + goto out; + } + + g_object_connect (self->proxy, + "swapped-object-signal::notify::bluetooth-airplane-mode", + G_CALLBACK (on_bt_airplane_mode_changed), + self, + "swapped-object-signal::notify::bluetooth-has-airplane-mode", + G_CALLBACK (on_bt_has_airplane_mode_changed), + self, + NULL); + on_bt_airplane_mode_changed (self, NULL, self->proxy); + on_bt_has_airplane_mode_changed (self, NULL, self->proxy); + + g_debug ("BT manager initialized"); +out: + g_object_unref (self); +} + + +static void +phosh_bt_manager_idle_init (PhoshManager *manager) +{ + PhoshBtManager *self = PHOSH_BT_MANAGER (manager); + + phosh_dbus_rfkill_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + BUS_NAME, + OBJECT_PATH, + NULL, + on_proxy_new_for_bus_finish, + g_object_ref (self)); +} + + +static void +phosh_bt_manager_dispose (GObject *object) +{ + PhoshBtManager *self = PHOSH_BT_MANAGER (object); + + g_clear_object (&self->proxy); + g_clear_object (&self->connectable_devices); + g_clear_object (&self->bt_client); + + g_clear_pointer (&self->info, g_free); + + G_OBJECT_CLASS (phosh_bt_manager_parent_class)->dispose (object); +} + + +static void +phosh_bt_manager_class_init (PhoshBtManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->dispose = phosh_bt_manager_dispose; + + object_class->get_property = phosh_bt_manager_get_property; + + manager_class->idle_init = phosh_bt_manager_idle_init; + + /** + * PhoshBtManager::icon-name: + * + * A icon name that indicates the current Bluetooth status. + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "icon name", + "The bt icon name", + "bluetooth-disabled-symbolic", + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager::enabled: + * + * Whether a Bluetooth is enabled. + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager::present: + * + * Whether a Bluetooth adapter is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:n-devices: + * + * The number of connectable Bluetooth devices + */ + props[PROP_N_DEVICES] = + g_param_spec_uint ("n-devices", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:n-connected: + * + * The number of currently connected Bluetooth devices + */ + props[PROP_N_CONNECTED] = + g_param_spec_uint ("n-connected", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshBtManager:info: + * + * If only a single device is connected this gives details about it. + */ + props[PROP_INFO] = + g_param_spec_string ("info", "", "", + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static gboolean +filter_devices (gpointer item, gpointer data) +{ + gboolean connectable; + BluetoothDevice *device = BLUETOOTH_DEVICE (item); + + g_object_get (device, "connectable", &connectable, NULL); + + return connectable; +} + + +static void +refilter_cb (PhoshBtManager *self) +{ + g_assert (PHOSH_IS_BT_MANAGER (self)); + + gtk_filter_list_model_refilter (self->connectable_devices); +} + + +static void +recount_cb (PhoshBtManager *self) +{ + g_autofree char *last_info = NULL; + guint n_devices, n_connected = 0; + + g_assert (PHOSH_IS_BT_MANAGER (self)); + + n_devices = g_list_model_get_n_items (G_LIST_MODEL (self->connectable_devices)); + for (int i = 0; i < n_devices; i++) { + g_autoptr (BluetoothDevice) device = NULL; + g_autofree char *info = NULL; + gboolean connected; + + device = g_list_model_get_item (G_LIST_MODEL (self->connectable_devices), i); + g_object_get (device, "connected", &connected, "alias", &info, NULL); + + if (connected) { + n_connected++; + last_info = g_steal_pointer (&info); + } + } + + if (g_strcmp0 (self->info, last_info)) { + g_debug ("New info: %s", last_info); + g_free (self->info); + self->info = g_steal_pointer (&last_info); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INFO]); + } + + if (self->n_connected != n_connected) { + g_debug ("%d Bluetooth devices connected", n_connected); + self->n_connected = n_connected; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_CONNECTED]); + } + + if (self->n_devices != n_devices) { + g_debug ("%d Bluetooth devices", n_devices); + self->n_devices = n_devices; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_DEVICES]); + } +} + + +static void +on_device_added (PhoshBtManager *self, BluetoothDevice *device) +{ + g_assert (PHOSH_IS_BT_MANAGER (self)); + g_assert (BLUETOOTH_IS_DEVICE (device)); + + g_signal_connect_swapped (device, "notify::connectable", G_CALLBACK (refilter_cb), self); + g_signal_connect_swapped (device, "notify::connected", G_CALLBACK (recount_cb), self); + + refilter_cb (self); + recount_cb (self); +} + + +static void +on_device_removed (PhoshBtManager *self, BluetoothDevice *device) +{ + refilter_cb (self); + recount_cb (self); +} + + +static void +setup_devices (PhoshBtManager *self) +{ + g_autoptr (GListStore) devices = NULL; + guint n_items; + + /* Keep a list of connectable devices */ + devices = bluetooth_client_get_devices (self->bt_client); + self->connectable_devices = gtk_filter_list_model_new (G_LIST_MODEL (devices), + filter_devices, + self, + NULL); + g_object_connect (self->bt_client, + "swapped-object-signal::device-added", on_device_added, self, + "swapped-object-signal::device-removed", on_device_removed, self, + NULL); + + /* cold plug existing devices */ + n_items = g_list_model_get_n_items (G_LIST_MODEL (devices)); + for (int i = 0; i < n_items; i++) { + g_autoptr (BluetoothDevice) device = NULL; + + device = g_list_model_get_item (G_LIST_MODEL (devices), i); + on_device_added (self, device); + } + + recount_cb (self); +} + + +static void +phosh_bt_manager_init (PhoshBtManager *self) +{ + self->icon_name = "bluetooth-disabled-symbolic"; + + self->bt_client = bluetooth_client_new (); + g_object_connect (self->bt_client, + "swapped-signal::notify::default-adapter-state", + on_adapter_state_changed, self, + "swapped-signal::notify::default-adapter-setup-mode", + on_adapter_setup_mode_changed, self, + NULL); + + setup_devices (self); +} + + +PhoshBtManager * +phosh_bt_manager_new (void) +{ + return PHOSH_BT_MANAGER (g_object_new (PHOSH_TYPE_BT_MANAGER, NULL)); +} + + +const char * +phosh_bt_manager_get_icon_name (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), NULL); + + return self->icon_name; +} + + +gboolean +phosh_bt_manager_get_enabled (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), FALSE); + + return self->enabled; +} + + +void +phosh_bt_manager_set_enabled (PhoshBtManager *self, gboolean enabled) +{ + g_return_if_fail (PHOSH_IS_BT_MANAGER (self)); + + if (!self->present) + return; + + if (enabled == self->enabled) + return; + + g_return_if_fail (self->proxy); + + self->enabled = enabled; + phosh_dbus_rfkill_set_bluetooth_airplane_mode (self->proxy, !enabled); +} + + +gboolean +phosh_bt_manager_get_present (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), FALSE); + + return self->present; +} + +/** + * phosh_bt_manager_get_connectable_devices: + * @self: The Bluetooth manager + * + * Gets the currently connectable devices. + * + * Returns:(transfer none): The connectable devices + */ +GListModel * +phosh_bt_manager_get_connectable_devices (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), NULL); + + return G_LIST_MODEL (self->connectable_devices); +} + + +guint +phosh_bt_manager_get_n_connected (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), 0); + + return self->n_connected; +} + + +const char * +phosh_bt_manager_get_info (PhoshBtManager *self) +{ + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (self), NULL); + + return self->info; +} + + +static void +on_service_connected (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GError *err = NULL; + GTask *task = G_TASK (user_data); + gboolean success; + + success = bluetooth_client_connect_service_finish (BLUETOOTH_CLIENT (source_object), res, &err); + if (!success) + g_debug ("Failed to connect: %s", err->message); + + if (!success) { + g_task_return_error (task, err); + return; + } + + g_task_return_boolean (task, success); +} + + +void +phosh_bt_manager_connect_device_async (PhoshBtManager *self, + BluetoothDevice *device, + gboolean connect, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data) +{ + const char *object_path; + GTask *task = g_task_new (self, cancellable, callback, user_data); + + object_path = bluetooth_device_get_object_path (device); + + g_debug ("%s device %s", connect ? "Connecting" : "Disconnecting", object_path); + bluetooth_client_connect_service (self->bt_client, + object_path, + connect, + cancellable, + on_service_connected, + task); +} + + +gboolean +phosh_bt_manager_connect_device_finish (PhoshBtManager *self, + GAsyncResult *result, + GError **error) +{ + g_autoptr (GTask) task = G_TASK (result); + + g_return_val_if_fail (g_task_is_valid (result, self), FALSE); + + return g_task_propagate_boolean (task, error); +} diff --git a/src/bt-manager.h b/src/bt-manager.h new file mode 100644 index 000000000..c664c12fe --- /dev/null +++ b/src/bt-manager.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include +#include + +/* Avoid including gnome-bluetooth headers here - they cause downstream issues for GIR consumers */ +typedef struct _BluetoothDevice BluetoothDevice; + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_MANAGER (phosh_bt_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshBtManager, phosh_bt_manager, PHOSH, BT_MANAGER, PhoshManager) + +PhoshBtManager *phosh_bt_manager_new (void); +const char *phosh_bt_manager_get_icon_name (PhoshBtManager *self); +gboolean phosh_bt_manager_get_enabled (PhoshBtManager *self); +void phosh_bt_manager_set_enabled (PhoshBtManager *self, gboolean enabled); +gboolean phosh_bt_manager_get_present (PhoshBtManager *self); +GListModel *phosh_bt_manager_get_connectable_devices (PhoshBtManager *self); +guint phosh_bt_manager_get_n_connected (PhoshBtManager *self); +const char *phosh_bt_manager_get_info (PhoshBtManager *self); +void phosh_bt_manager_connect_device_async (PhoshBtManager *self, + BluetoothDevice *device, + gboolean connect, + GAsyncReadyCallback callback, + GCancellable *cancellable, + gpointer user_data); +gboolean phosh_bt_manager_connect_device_finish (PhoshBtManager *self, + GAsyncResult *result, + GError **error); + +G_END_DECLS diff --git a/src/bt-status-page.c b/src/bt-status-page.c new file mode 100644 index 000000000..0797a1d62 --- /dev/null +++ b/src/bt-status-page.c @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-bt-status-page" + +#include "bt-device-row.h" +#include "bt-status-page.h" +#include "status-page-placeholder.h" + +#include "bluetooth-device.h" + +#include + +/** + * PhoshBtStatusPage: + * + * A Quick setting status page widget to show Bluetooth devices + */ + +struct _PhoshBtStatusPage { + PhoshStatusPage parent_instance; + + GtkListBox *devices_list_box; + GtkStack *stack; + PhoshStatusPagePlaceholder *empty_state; + GtkButton *enable_button; + guint activating; + + PhoshBtManager *bt_manager; +}; + +G_DEFINE_TYPE (PhoshBtStatusPage, phosh_bt_status_page, PHOSH_TYPE_STATUS_PAGE); + + +static void +on_row_activating (PhoshBtStatusPage *self) +{ + self->activating++; +} + + +static void +on_row_activated (PhoshBtStatusPage *self, gboolean success) +{ + g_return_if_fail (self->activating); + + self->activating--; + + if (self->activating == 0 && success) + g_signal_emit_by_name (self, "done", NULL); +} + + +static GtkWidget * +create_bt_device_row (gpointer item, gpointer user_data) +{ + PhoshBtStatusPage *self = PHOSH_BT_STATUS_PAGE (user_data); + BluetoothDevice *device = BLUETOOTH_DEVICE (item); + GtkWidget *row = phosh_bt_device_row_new (device); + + g_object_connect (row, + "swapped-object-signal::activating", on_row_activating, self, + "swapped-object-signal::done", on_row_activated, self, + NULL); + return row; +} + + +static void +on_enable_clicked (PhoshBtStatusPage *self) +{ + phosh_bt_manager_set_enabled (self->bt_manager, TRUE); +} + + +static void +phosh_bt_status_page_dispose (GObject *object) +{ + PhoshBtStatusPage *self = PHOSH_BT_STATUS_PAGE (object); + + g_clear_object (&self->bt_manager); + + G_OBJECT_CLASS (phosh_bt_status_page_parent_class)->dispose (object); +} + + +static void +phosh_bt_status_page_class_init (PhoshBtStatusPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_bt_status_page_dispose; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/bt-status-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, empty_state); + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, devices_list_box); + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, enable_button); + gtk_widget_class_bind_template_child (widget_class, PhoshBtStatusPage, stack); + + gtk_widget_class_bind_template_callback (widget_class, on_enable_clicked); +} + + +static void +update_stack_page_cb (PhoshBtStatusPage *self) +{ + const char *page_name, *title; + gboolean show_enable_button; + + if (phosh_bt_manager_get_enabled (self->bt_manager)) { + GListModel *devices = phosh_bt_manager_get_connectable_devices (self->bt_manager); + guint n_devices = g_list_model_get_n_items (devices); + + show_enable_button = FALSE; + title = _("No connectable Bluetooth Devices found"); + page_name = n_devices ? "devices" : "empty-state"; + } else { + show_enable_button = TRUE; + title = _("Bluetooth disabled"); + page_name = "empty-state"; + } + + phosh_status_page_placeholder_set_title (self->empty_state, title); + gtk_stack_set_visible_child_name (self->stack, page_name); + gtk_widget_set_visible (GTK_WIDGET (self->enable_button), show_enable_button); +} + + +static void +phosh_bt_status_page_init (PhoshBtStatusPage *self) +{ + PhoshShell *shell; + + gtk_widget_init_template (GTK_WIDGET (self)); + + shell = phosh_shell_get_default (); + self->bt_manager = g_object_ref (phosh_shell_get_bt_manager (shell)); + g_return_if_fail (PHOSH_IS_BT_MANAGER (self->bt_manager)); + + gtk_list_box_bind_model (self->devices_list_box, + phosh_bt_manager_get_connectable_devices (self->bt_manager), + create_bt_device_row, + self, + NULL); + + g_signal_connect_object (self->bt_manager, "notify::n-devices", + G_CALLBACK (update_stack_page_cb), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->bt_manager, "notify::enabled", + G_CALLBACK (update_stack_page_cb), self, G_CONNECT_SWAPPED); + update_stack_page_cb (self); +} + + +GtkWidget * +phosh_bt_status_page_new (void) +{ + return g_object_new (PHOSH_TYPE_BT_STATUS_PAGE, NULL); +} diff --git a/src/bt-status-page.h b/src/bt-status-page.h new file mode 100644 index 000000000..113190b6d --- /dev/null +++ b/src/bt-status-page.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "quick-setting.h" +#include "shell-priv.h" +#include "status-page.h" + +#include + + +G_BEGIN_DECLS + +#define PHOSH_TYPE_BT_STATUS_PAGE phosh_bt_status_page_get_type () +G_DECLARE_FINAL_TYPE (PhoshBtStatusPage, phosh_bt_status_page, PHOSH, BT_STATUS_PAGE, + PhoshStatusPage) + +GtkWidget *phosh_bt_status_page_new (void); + +G_END_DECLS diff --git a/src/call-notification.c b/src/call-notification.c new file mode 100644 index 000000000..60a713f94 --- /dev/null +++ b/src/call-notification.c @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-call-notification" + +#include "phosh-config.h" + +#include "call-notification.h" +#include "calls-manager.h" +#include "util.h" + +#include + +#include +#include +#include + +/** + * PhoshCallNotification: + * + * The notifictaion shown when a call is ongoing. The call is set at + * construction time and can't be changed. + */ + +enum { + PROP_0, + PROP_CALL, + PROP_ACTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshCallNotification { + GtkListBoxRow parent; + + HdyAvatar *avatar; + GtkLabel *call_duration; + GtkLabel *call_state; + GtkLabel *caller; + GtkLabel *caller_detail; + + PhoshCall *call; + gboolean active; +}; +G_DEFINE_TYPE (PhoshCallNotification, phosh_call_notification, GTK_TYPE_LIST_BOX_ROW) + + +static void +on_caller_info_changed (PhoshCallNotification *self, GParamSpec *pspec, PhoshCall *call) +{ + const char *caller, *caller_detail; + const char *display_name = cui_call_get_display_name (CUI_CALL (call)); + const char *id = cui_call_get_id (CUI_CALL (call)); + + g_debug ("%s %s", display_name, id); + if (gm_str_is_null_or_empty (display_name)) { + caller_detail = NULL; + caller = gm_str_is_null_or_empty (id) ? _("Unknown caller") : id; + } else { + caller = display_name; + caller_detail = id; + } + + gtk_label_set_label (self->caller, caller); + gtk_label_set_label (self->caller_detail, caller_detail); +} + + +static gboolean +transform_label_to_visible (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + const char *label = g_value_get_string (from_value); + gboolean visible = TRUE; + + /* Hide details for unknown callers so the display name is centered */ + if (gm_str_is_null_or_empty (label)) + visible = FALSE; + + g_value_set_boolean (to_value, visible); + return TRUE; +} + + +static gboolean +transform_display_name_initals (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + const char *display_name = g_value_get_string (from_value); + gboolean show_initials = TRUE; + + /* Don't show initials for unknown callers as it would always be 'UC' */ + if (gm_str_is_null_or_empty (display_name)) + show_initials = FALSE; + + g_value_set_boolean (to_value, show_initials); + return TRUE; +} + + +static gboolean +transform_active_time (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + double elapsed = g_value_get_double (from_value); + + g_value_take_string (to_value, cui_call_format_duration (elapsed)); + return TRUE; +} + + +static gboolean +transform_call_state (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshCallState state = g_value_get_enum (from_value); + + g_value_set_string (to_value, cui_call_state_to_string ((CuiCallState)state)); + return TRUE; +} + + +static void +phosh_call_notification_set_call (PhoshCallNotification *self, PhoshCall *call) +{ + g_return_if_fail (PHOSH_IS_CALL_NOTIFICATION (self)); + g_return_if_fail (PHOSH_IS_CALL (call)); + g_return_if_fail (self->call == NULL); + + g_set_object (&self->call, call); + g_object_connect (call, + "swapped-object-signal::notify::display-name", on_caller_info_changed, self, + "swapped-object-signal::notify::id", on_caller_info_changed, self, + NULL); + on_caller_info_changed (self, NULL, call); + + g_object_bind_property (call, "display-name", + self->avatar, "text", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + g_object_bind_property_full (call, "display-name", + self->avatar, "show-initials", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_display_name_initals, + NULL, + NULL, + NULL); + g_object_bind_property (call, "avatar-icon", + self->avatar, "loadable-icon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + g_object_bind_property_full (call, "active-time", + self->call_duration, "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_active_time, + NULL, + NULL, + NULL); + g_object_bind_property_full (call, "state", + self->call_state, "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_call_state, + NULL, + NULL, + NULL); + + /* Only show detail label when non-empty */ + g_object_bind_property_full (self->caller_detail, "label", + self->caller_detail, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_label_to_visible, + NULL, + NULL, + NULL); +} + + +static void +phosh_call_notification_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCallNotification *self = PHOSH_CALL_NOTIFICATION (object); + + switch (property_id) { + case PROP_CALL: + phosh_call_notification_set_call (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_call_notification_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCallNotification *self = PHOSH_CALL_NOTIFICATION (object); + + switch (property_id) { + case PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_call_notification_dispose (GObject *object) +{ + PhoshCallNotification *self = PHOSH_CALL_NOTIFICATION (object); + + g_clear_object (&self->call); + + G_OBJECT_CLASS (phosh_call_notification_parent_class)->dispose (object); +} + + +static void +phosh_call_notification_class_init (PhoshCallNotificationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_call_notification_get_property; + object_class->set_property = phosh_call_notification_set_property; + object_class->dispose = phosh_call_notification_dispose; + + /** + * PhoshCallNotification:call: + * + * The call tracked by this notification + */ + props[PROP_CALL] = + g_param_spec_object ("call", "", "", + PHOSH_TYPE_CALL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * PhoshCallNotification:active: + * + * %TRUE when the notification has an associated call and it is active. + */ + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/call-notification.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshCallNotification, avatar); + gtk_widget_class_bind_template_child (widget_class, PhoshCallNotification, call_duration); + gtk_widget_class_bind_template_child (widget_class, PhoshCallNotification, call_state); + gtk_widget_class_bind_template_child (widget_class, PhoshCallNotification, caller); + gtk_widget_class_bind_template_child (widget_class, PhoshCallNotification, caller_detail); + + gtk_widget_class_set_css_name (widget_class, "phosh-call-notification"); +} + + +static void +phosh_call_notification_init (PhoshCallNotification *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshCallNotification * +phosh_call_notification_new (PhoshCall *call) +{ + return g_object_new (PHOSH_TYPE_CALL_NOTIFICATION, + "call", call, + NULL); +} diff --git a/src/call-notification.h b/src/call-notification.h new file mode 100644 index 000000000..6fb396173 --- /dev/null +++ b/src/call-notification.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "call.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CALL_NOTIFICATION (phosh_call_notification_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshCallNotification, phosh_call_notification, PHOSH, CALL_NOTIFICATION, + GtkListBoxRow) + +PhoshCallNotification *phosh_call_notification_new (PhoshCall *call); + +G_END_DECLS diff --git a/src/call.c b/src/call.c new file mode 100644 index 000000000..e87441a70 --- /dev/null +++ b/src/call.c @@ -0,0 +1,493 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-call" + +#include "call.h" +#include "util.h" + +#include +#include + +/** + * PhoshCall: + * + * A phone call + * + * Interfaces with a single call object on DBus. GNOME Calls exports + * information of phone calls on DBus. This class interfaces with one + * of them to provide the necessary information to e.g. handle calls + * on the lock screen. + */ + + +enum { + PROP_0, + PROP_DBUS_PROXY, + PROP_NUM_OBJ_PROPS, + /* From the cui-call interface */ + PROP_DISPLAY_NAME = PROP_NUM_OBJ_PROPS, + PROP_AVATAR_ICON, + PROP_ID, + PROP_STATE, + PROP_ENCRYPTED, + PROP_CAN_DTMF, + PROP_ACTIVE_TIME, + PROP_NUM_PROPS, +}; +static GParamSpec *props[PROP_NUM_PROPS]; + + +typedef struct _PhoshCall { + GObject parent; + + PhoshDBusCallsCall *proxy; /* DBus proxy to a single call on gnome-calls' DBus service */ + GCancellable *cancel; + + GLoadableIcon *avatar_icon; + gboolean can_dtmf; + + GTimer *timer; + gdouble active_time; + guint timer_id; +} PhoshCall; + + +static void phosh_call_cui_call_interface_init (CuiCallInterface *iface); +G_DEFINE_TYPE_WITH_CODE (PhoshCall, phosh_call, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (CUI_TYPE_CALL, + phosh_call_cui_call_interface_init)) + + +static GLoadableIcon * +phosh_call_get_avatar_icon (CuiCall *call) +{ + g_return_val_if_fail (PHOSH_IS_CALL (call), NULL); + + return PHOSH_CALL (call)->avatar_icon; +} + + +static const char * +phosh_call_get_id (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), NULL); + self = PHOSH_CALL (call); + + return phosh_dbus_calls_call_get_id (self->proxy); +} + + +static const char * +phosh_call_get_display_name (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), NULL); + self = PHOSH_CALL (call); + + return phosh_dbus_calls_call_get_display_name (self->proxy); +} + + +static CuiCallState +phosh_call_get_state (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), CUI_CALL_STATE_UNKNOWN); + self = PHOSH_CALL (call); + + return phosh_dbus_calls_call_get_state (self->proxy); +} + + +static gboolean +phosh_call_get_encrypted (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), CUI_CALL_STATE_UNKNOWN); + self = PHOSH_CALL (call); + + return phosh_dbus_calls_call_get_encrypted (self->proxy); +} + + +static gboolean +phosh_call_get_can_dtmf (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), CUI_CALL_STATE_UNKNOWN); + self = PHOSH_CALL (call); + + return phosh_dbus_calls_call_get_can_dtmf (self->proxy); +} + + +static gdouble +phosh_call_get_active_time (CuiCall *call) +{ + PhoshCall *self; + + g_return_val_if_fail (PHOSH_IS_CALL (call), 0.0); + self = PHOSH_CALL (call); + + return self->active_time; +} + + +static void +on_prop_changed (PhoshCall *self, GParamSpec *pspec) +{ + const char *name = g_param_spec_get_name (pspec); + + /* Just forward any property changes, we fetch them from the DBus proxy anyway */ + if (g_strcmp0 (name, "encrypted") == 0 || + g_strcmp0 (name, "id") == 0 || + g_strcmp0 (name, "display-name") == 0 || + g_strcmp0 (name, "can-dtmf")) { + g_object_notify (G_OBJECT (self), name); + } +} + + +static gboolean +on_active_time_ticked (gpointer data) +{ + PhoshCall *self = PHOSH_CALL (data); + + self->active_time = g_timer_elapsed (self->timer, NULL); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE_TIME]); + + return G_SOURCE_CONTINUE; +} + + +static void +on_state_changed (PhoshCall *self) +{ + + /* Check for started timer, because state could have changed like this: + * ACTIVE -> HELD -> ACTIVE + * and we don't want to start the timer multiple times. + * We only stop tracking the active time when the call disconnects. + */ + if (cui_call_get_state (CUI_CALL (self)) == CUI_CALL_STATE_ACTIVE && + !self->timer) { + self->timer = g_timer_new (); + self->timer_id = g_timeout_add (500, on_active_time_ticked, self); + g_source_set_name_by_id (self->timer_id, "[phosh] call timeout"); + } else if (cui_call_get_state (CUI_CALL (self)) == CUI_CALL_STATE_DISCONNECTED) { + g_clear_handle_id (&self->timer_id, g_source_remove); + g_clear_pointer (&self->timer, g_timer_destroy); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); +} + + +static void +phosh_call_set_dbus_proxy (PhoshCall *self, PhoshDBusCallsCall *proxy) +{ + self->proxy = g_object_ref (proxy); + + g_object_connect (self->proxy, + "swapped-signal::notify::state", G_CALLBACK (on_state_changed), self, + "swapped-signal::notify::encrypted", G_CALLBACK (on_prop_changed), self, + "swapped-signal::notify::id", G_CALLBACK (on_prop_changed), self, + "swapped-signal::notify::display-name", G_CALLBACK (on_prop_changed), self, + "swapped-signal::notify::can-dtmf", G_CALLBACK (on_prop_changed), self, + NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DBUS_PROXY]); +} + + +static void +phosh_call_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCall *self = PHOSH_CALL (object); + + switch (property_id) { + case PROP_DBUS_PROXY: + /* construct only */ + phosh_call_set_dbus_proxy (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_call_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCall *self = PHOSH_CALL (object); + CuiCall *iface = CUI_CALL (object); + + switch (property_id) { + case PROP_DBUS_PROXY: + g_value_set_object (value, self->proxy); + break; + case PROP_AVATAR_ICON: + g_value_set_object (value, self->avatar_icon); + break; + case PROP_ID: + g_value_set_string (value, phosh_call_get_id (iface)); + break; + case PROP_DISPLAY_NAME: + g_value_set_string (value, phosh_call_get_display_name (iface)); + break; + case PROP_STATE: + g_value_set_enum (value, phosh_call_get_state (iface)); + break; + case PROP_ENCRYPTED: + g_value_set_boolean (value, phosh_call_get_encrypted (iface)); + break; + case PROP_CAN_DTMF: + g_value_set_boolean (value, phosh_call_get_can_dtmf (iface)); + break; + case PROP_ACTIVE_TIME: + g_value_set_double (value, phosh_call_get_active_time (iface)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_call_constructed (GObject *object) +{ + PhoshCall *self = PHOSH_CALL (object); + g_autoptr (GFile) file = NULL; + const char *path = NULL; + + G_OBJECT_CLASS (phosh_call_parent_class)->constructed (object); + + path = phosh_dbus_calls_call_get_image_path (self->proxy); + if (!gm_str_is_null_or_empty (path)) { + file = g_file_new_for_path (path); + if (file) + self->avatar_icon = G_LOADABLE_ICON (g_file_icon_new (file)); + } + + /* Sync active property */ + on_state_changed (self); +} + + +static void +phosh_call_dispose (GObject *object) +{ + PhoshCall *self = PHOSH_CALL (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_signal_handlers_disconnect_by_data (self->proxy, self); + g_clear_object (&self->proxy); + g_clear_object (&self->avatar_icon); + g_clear_handle_id (&self->timer_id, g_source_remove); + g_clear_pointer (&self->timer, g_timer_destroy); + + G_OBJECT_CLASS (phosh_call_parent_class)->dispose (object); +} + + +static void +phosh_call_class_init (PhoshCallClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_call_constructed; + object_class->dispose = phosh_call_dispose; + object_class->set_property = phosh_call_set_property; + object_class->get_property = phosh_call_get_property; + + /** + * PhoshCall:dbus-proxy: + * + * The DBus proxy object to a call on gnome-calls DBus interface + */ + props[PROP_DBUS_PROXY] = g_param_spec_object ("dbus-proxy", + "", + "", + PHOSH_DBUS_TYPE_CALLS_CALL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties (object_class, PROP_NUM_OBJ_PROPS, props); + + g_object_class_override_property (object_class, + PROP_AVATAR_ICON, + "avatar-icon"); + props[PROP_AVATAR_ICON] = g_object_class_find_property (object_class, "avatar-icon"); + + g_object_class_override_property (object_class, + PROP_ID, + "id"); + props[PROP_ID] = g_object_class_find_property (object_class, "id"); + + g_object_class_override_property (object_class, + PROP_DISPLAY_NAME, + "display-name"); + props[PROP_DISPLAY_NAME] = g_object_class_find_property (object_class, "display-name"); + + g_object_class_override_property (object_class, + PROP_STATE, + "state"); + props[PROP_STATE] = g_object_class_find_property (object_class, "state"); + + g_object_class_override_property (object_class, + PROP_ENCRYPTED, + "encrypted"); + props[PROP_ENCRYPTED] = g_object_class_find_property (object_class, "encrypted"); + + g_object_class_override_property (object_class, + PROP_CAN_DTMF, + "can-dtmf"); + props[PROP_CAN_DTMF] = g_object_class_find_property (object_class, "can-dtmf"); + + g_object_class_override_property (object_class, + PROP_ACTIVE_TIME, + "active-time"); + props[PROP_ACTIVE_TIME] = g_object_class_find_property (object_class, "active-time"); +} + + +static void +on_call_accept_finish (GObject *source_object, GAsyncResult *res, gpointer unused) +{ + PhoshDBusCallsCall *proxy = PHOSH_DBUS_CALLS_CALL (source_object); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_CALLS_CALL_PROXY (proxy)); + + if (!phosh_dbus_calls_call_call_accept_finish (proxy, res, &err)) + phosh_async_error_warn (err, "Failed to accept call %p", proxy); +} + + +static void +phosh_call_accept (CuiCall *call) +{ + PhoshCall *self; + + g_return_if_fail (PHOSH_IS_CALL (call)); + self = PHOSH_CALL (call); + + phosh_dbus_calls_call_call_accept (self->proxy, + self->cancel, + on_call_accept_finish, + NULL); +} + +static void +on_call_hangup_finish (GObject *source_object, GAsyncResult *res, gpointer unused) +{ + PhoshDBusCallsCall *proxy = PHOSH_DBUS_CALLS_CALL (source_object); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_CALLS_CALL_PROXY (proxy)); + + if (!phosh_dbus_calls_call_call_hangup_finish (proxy, res, &err)) + phosh_async_error_warn (err, "Failed to hangup call %p", proxy); +} + + +static void +phosh_call_hang_up (CuiCall *call) +{ + PhoshCall *self; + + g_return_if_fail (PHOSH_IS_CALL (call)); + self = PHOSH_CALL (call); + + phosh_dbus_calls_call_call_hangup (self->proxy, + self->cancel, + on_call_hangup_finish, + NULL); +} + + +static void +on_call_send_dtmf_finish (GObject *source_object, GAsyncResult *res, gpointer dtmf_key) +{ + PhoshDBusCallsCall *proxy = PHOSH_DBUS_CALLS_CALL (source_object); + g_autoptr (GError) err = NULL; + char key = (char) GPOINTER_TO_INT (dtmf_key); + + g_return_if_fail (PHOSH_DBUS_IS_CALLS_CALL_PROXY (proxy)); + + if (!phosh_dbus_calls_call_call_send_dtmf_finish (proxy, res, &err)) + phosh_async_error_warn (err, "Failed to send DTMF `%c' %p", key, proxy); +} + + +static void +phosh_call_send_dtmf (CuiCall *call, const char *dtmf) +{ + PhoshCall *self; + + g_return_if_fail (PHOSH_IS_CALL (call)); + + self = PHOSH_CALL (call); + + phosh_dbus_calls_call_call_send_dtmf (self->proxy, + dtmf, + self->cancel, + on_call_send_dtmf_finish, + GINT_TO_POINTER (dtmf)); +} + + +static void +phosh_call_cui_call_interface_init (CuiCallInterface *iface) +{ + iface->get_avatar_icon = phosh_call_get_avatar_icon; + iface->get_id = phosh_call_get_id; + iface->get_display_name = phosh_call_get_display_name; + iface->get_state = phosh_call_get_state; + iface->get_encrypted = phosh_call_get_encrypted; + iface->get_can_dtmf = phosh_call_get_can_dtmf; + iface->get_active_time = phosh_call_get_active_time; + + iface->accept = phosh_call_accept; + iface->hang_up = phosh_call_hang_up; + iface->send_dtmf = phosh_call_send_dtmf; +} + + +static void +phosh_call_init (PhoshCall *self) +{ + self->cancel = g_cancellable_new (); + + /* TODO: once DBus handles it */ + self->can_dtmf = FALSE; +} + + +PhoshCall * +phosh_call_new (PhoshDBusCallsCall *proxy) +{ + return g_object_new (PHOSH_TYPE_CALL, + "dbus-proxy", proxy, + NULL); +} diff --git a/src/call.h b/src/call.h new file mode 100644 index 000000000..36ef11c89 --- /dev/null +++ b/src/call.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "dbus/calls-dbus.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CALL (phosh_call_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshCall, phosh_call, PHOSH, CALL, GObject) + +PhoshCall * phosh_call_new (PhoshDBusCallsCall *proxy); + +G_END_DECLS diff --git a/src/calls-manager.c b/src/calls-manager.c new file mode 100644 index 000000000..07e62dfcb --- /dev/null +++ b/src/calls-manager.c @@ -0,0 +1,539 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-calls-manager" + +#include "phosh-config.h" + +#include "call.h" +#include "calls-manager.h" +#include "shell-priv.h" +#include "util.h" +#include "dbus/calls-dbus.h" + +#define BUS_NAME "org.gnome.Calls" +#define OBJECT_PATH "/org/gnome/Calls" +#define OBJECT_PATHS_CALLS_PREFIX OBJECT_PATH "/Call/" + +/** + * PhoshCallsManager: + * + * Track ongoing phone calls + * + * #PhoshCallsManager tracks on going calls on the org.gnome.Calls DBus + * interface and allows interaction with them by wrapping the + * #PhoshCallsDBusCallsCall proxies in #PhoshCall so all DBus and + * ObjectManager related logic stays local within #PhoshCallsManager. + */ + +enum { + PROP_0, + PROP_PRESENT, + PROP_ACTIVE_CALL, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + CALL_ADDED, + CALL_REMOVED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +struct _PhoshCallsManager { + PhoshManager parent; + + gboolean present; + gboolean incoming; + char *active_call; + + PhoshDBusObjectManagerClient *om_client; + GCancellable *cancel; + GHashTable *calls; + GListStore *calls_store; +}; + +static void phosh_calls_manager_list_model_iface_init (GListModelInterface *iface); +G_DEFINE_TYPE_WITH_CODE (PhoshCallsManager, phosh_calls_manager, PHOSH_TYPE_MANAGER, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, + phosh_calls_manager_list_model_iface_init)) + +static gboolean +is_active (PhoshCallState state) +{ + gboolean ret = FALSE; + + if (state == PHOSH_CALL_STATE_ACTIVE || + state == PHOSH_CALL_STATE_INCOMING || + state == PHOSH_CALL_STATE_DIALING) + ret = TRUE; + + return ret; +} + + +static void +on_call_state_changed (PhoshCallsManager *self, GParamSpec *pspec, PhoshDBusCallsCall *proxy) +{ + const char *path; + PhoshCallState state; + PhoshCall *call; + + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_CALLS_CALL (proxy)); + + call = g_object_get_data (G_OBJECT (proxy), "call"); + g_return_if_fail (call); + + path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (proxy)); + state = phosh_dbus_calls_call_get_state (proxy); + + g_debug ("Call %s, state %d", path, state); + if (g_strcmp0 (path, self->active_call) == 0) { + + self->incoming = state == PHOSH_CALL_STATE_INCOMING; + /* current active call became inactive */ + if (!is_active (state)) { + g_debug ("No active call, was %s", path); + + g_clear_pointer (&self->active_call, g_free); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE_CALL]); + /* TODO: pick new active call from list once calls supports multiple active calls */ + } + return; + } + + if (!is_active (state)) + return; + + /* New active call */ + g_free (self->active_call); + self->active_call = g_strdup (path); + g_debug ("New active call %s", path); + self->incoming = state == PHOSH_CALL_STATE_INCOMING; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE_CALL]); +} + + +static void +on_call_proxy_new_for_bus_finish (GObject *source_object, GAsyncResult *res, gpointer data) +{ + const char *path; + gboolean inbound; + PhoshCallsManager *self; + g_autoptr (PhoshDBusCallsCall) proxy = NULL; + g_autoptr (PhoshCall) call = NULL; + g_autoptr (GError) err = NULL; + + proxy = phosh_dbus_calls_call_proxy_new_for_bus_finish (res, &err); + if (!proxy) { + phosh_async_error_warn (err, "Failed to get call proxy"); + return; + } + + self = PHOSH_CALLS_MANAGER (data); + path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (proxy)); + if (g_hash_table_contains (self->calls, path)) { + g_warning ("Already got a call with path %s", path); + return; + } + + /* Wrap DBus proxy in PhoshCall */ + call = phosh_call_new (proxy); + g_object_set_data (G_OBJECT (proxy), "call", call); + g_hash_table_insert (self->calls, g_strdup (path), call); + g_list_store_append (self->calls_store, call); + + g_signal_connect_swapped (proxy, + "notify::state", + G_CALLBACK (on_call_state_changed), + self); + on_call_state_changed (self, NULL, proxy); + + inbound = phosh_dbus_calls_call_get_inbound (proxy); + g_debug ("Added call %s, inbound: %d", path, inbound); + + g_signal_emit (self, signals[CALL_ADDED], 0, path); +} + + +static void +on_call_obj_added (PhoshCallsManager *self, GDBusObject *object) +{ + const char *path; + + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (self)); + + path = g_dbus_object_get_object_path (object); + g_debug ("New call obj at %s", path); + if (!g_str_has_prefix (path, OBJECT_PATHS_CALLS_PREFIX)) + return; + + phosh_dbus_calls_call_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + BUS_NAME, + path, + self->cancel, + on_call_proxy_new_for_bus_finish, + self); +} + + +static void +remove_call_by_path (PhoshCallsManager *self, const char *path) +{ + PhoshCall *call; + guint pos; + + /* Disposes the call object by removing it from the list store and + hash table thus disposing the proxy as well */ + call = g_hash_table_lookup (self->calls, path); + g_return_if_fail (call); + + g_hash_table_remove (self->calls, path); + + g_return_if_fail (g_list_store_find (self->calls_store, call, &pos)); + g_list_store_remove (self->calls_store, pos); +} + + +static void +on_call_obj_removed (PhoshCallsManager *self, + GDBusObject *object) +{ + const char *path; + + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (self)); + + path = g_dbus_object_get_object_path (object); + g_debug ("Call obj at %s gone", path); + if (!g_str_has_prefix (path, OBJECT_PATHS_CALLS_PREFIX)) + return; + + if (g_strcmp0 (path, self->active_call) == 0) { + g_clear_pointer (&self->active_call, g_free); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE_CALL]); + /* TODO: pick new active call from list once calls supports multiple active calls */ + } + + g_debug ("Removed call %s", path); + g_signal_emit (self, signals[CALL_REMOVED], 0, path); + + remove_call_by_path (self, path); +} + + +static void +phosh_calls_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCallsManager *self = PHOSH_CALLS_MANAGER (object); + + switch (property_id) { + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_ACTIVE_CALL: + g_value_set_string (value, self->active_call); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_name_owner_changed (PhoshCallsManager *self, + GParamSpec *pspec, + GDBusObjectManagerClient *om) +{ + g_autofree char *owner = NULL; + gboolean present; + + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_OBJECT_MANAGER_CLIENT (om)); + + owner = g_dbus_object_manager_client_get_name_owner (om); + present = owner ? TRUE : FALSE; + + if (present) { + g_autolist (GDBusObject) objs = g_dbus_object_manager_get_objects ( + G_DBUS_OBJECT_MANAGER (self->om_client)); + + /* Catch up on ongoing calls */ + for (GList *elem = objs; elem; elem = elem->next) { + on_call_obj_added (self, elem->data); + } + } /* else {} is not necessary since we get object-removed signals + * when name owner quits + */ + + if (present != self->present) { + self->present = present; + g_debug ("Calls present: %d", self->present); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); + } +} + + +static void +on_om_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + gpointer data) +{ + g_autoptr (GError) err = NULL; + PhoshCallsManager *self; + GDBusObjectManager *om; + GDBusObjectManagerClient *om_client; + + om = phosh_dbus_object_manager_client_new_for_bus_finish (res, &err); + if (om == NULL) { + g_message ("Failed to get calls object manager client: %s", err->message); + return; + } + + self = PHOSH_CALLS_MANAGER (data); + self->om_client = PHOSH_DBUS_OBJECT_MANAGER_CLIENT (om); + om_client = G_DBUS_OBJECT_MANAGER_CLIENT (om); + + g_signal_connect_object (self->om_client, + "notify::name-owner", + G_CALLBACK (on_name_owner_changed), + self, + G_CONNECT_SWAPPED); + on_name_owner_changed (self, NULL, G_DBUS_OBJECT_MANAGER_CLIENT (om)); + + g_signal_connect_object (self->om_client, + "object-added", + G_CALLBACK (on_call_obj_added), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->om_client, + "object-removed", + G_CALLBACK (on_call_obj_removed), + self, + G_CONNECT_SWAPPED); + + g_debug ("Calls manager initialized for name %s at %s", + g_dbus_object_manager_client_get_name (om_client), + g_dbus_object_manager_get_object_path (om)); +} + + +static void +on_items_changed (PhoshCallsManager *self, + guint position, + guint removed, + guint added, + GListStore *calls_store) +{ + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); +} + + +static GType +phosh_calls_manager_list_model_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_CALL; +} + + +static gpointer +phosh_calls_manager_list_model_get_item (GListModel *list, guint position) +{ + PhoshCallsManager *self = PHOSH_CALLS_MANAGER (list); + + return g_list_model_get_item (G_LIST_MODEL (self->calls_store), position); +} + + +static unsigned int +phosh_calls_manager_list_model_get_n_items (GListModel *list) +{ + PhoshCallsManager *self = PHOSH_CALLS_MANAGER (list); + + return g_list_model_get_n_items (G_LIST_MODEL (self->calls_store)); +} + + +static void +phosh_calls_manager_list_model_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = phosh_calls_manager_list_model_get_item_type; + iface->get_item = phosh_calls_manager_list_model_get_item; + iface->get_n_items = phosh_calls_manager_list_model_get_n_items; +} + + +static void +phosh_calls_manager_idle_init (PhoshManager *manager) +{ + PhoshCallsManager *self = PHOSH_CALLS_MANAGER (manager); + + phosh_dbus_object_manager_client_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_DO_NOT_AUTO_START, + BUS_NAME, + OBJECT_PATH, + self->cancel, + on_om_new_for_bus_finish, + self); +} + + +static void +phosh_calls_manager_dispose (GObject *object) +{ + PhoshCallsManager *self = PHOSH_CALLS_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->om_client); + g_clear_object (&self->calls_store); + g_clear_pointer (&self->calls, g_hash_table_unref); + g_clear_pointer (&self->active_call, g_free); + + G_OBJECT_CLASS (phosh_calls_manager_parent_class)->dispose (object); +} + + +static void +phosh_calls_manager_class_init (PhoshCallsManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->get_property = phosh_calls_manager_get_property; + object_class->dispose = phosh_calls_manager_dispose; + + manager_class->idle_init = phosh_calls_manager_idle_init; + + /** + * PhoshCallsManager:present: + * + * Whether the call interface is present on the bus + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", + "", + "", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * PhoshCallsManager:active-call: + * + * The currently active call + */ + props[PROP_ACTIVE_CALL] = + g_param_spec_string ("active-call", + "", + "", + NULL, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CALL_ADDED] = g_signal_new ("call-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + + signals[CALL_REMOVED] = g_signal_new ("call-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); +} + + +static void +phosh_calls_manager_init (PhoshCallsManager *self) +{ + self->calls = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + self->calls_store = g_list_store_new (PHOSH_TYPE_CALL); + g_signal_connect_object (self->calls_store, "items-changed", + G_CALLBACK (on_items_changed), self, + G_CONNECT_SWAPPED); + + self->cancel = g_cancellable_new (); +} + + +PhoshCallsManager * +phosh_calls_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_CALLS_MANAGER, NULL); +} + + +gboolean +phosh_calls_manager_get_present (PhoshCallsManager *self) +{ + g_return_val_if_fail (PHOSH_IS_CALLS_MANAGER (self), FALSE); + + return self->present; +} + +/** + * phosh_calls_manager_has_incoming_call: + * @self: The calls manager + * + * Whether there's currently an incoming call + * + * Returns: `TRUE` if there's an incoming call + */ +gboolean +phosh_calls_manager_has_incoming_call (PhoshCallsManager *self) +{ + g_return_val_if_fail (PHOSH_IS_CALLS_MANAGER (self), FALSE); + + return self->incoming; +} + + +const char * +phosh_calls_manager_get_active_call_handle (PhoshCallsManager *self) +{ + g_return_val_if_fail (PHOSH_IS_CALLS_MANAGER (self), NULL); + + return self->active_call; +} + +/** + * phosh_calls_manager_get_call: + * @self: The calls manager + * @handle: The handle identifying the call + * + * Get the call associated with the given handle + * + * Returns:(transfer none): The call + */ +PhoshCall * +phosh_calls_manager_get_call (PhoshCallsManager *self, const char *handle) +{ + g_return_val_if_fail (PHOSH_IS_CALLS_MANAGER (self), NULL); + + return g_hash_table_lookup (self->calls, handle); +} diff --git a/src/calls-manager.h b/src/calls-manager.h new file mode 100644 index 000000000..a5656cee9 --- /dev/null +++ b/src/calls-manager.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "call.h" +#include "manager.h" + +#include + +G_BEGIN_DECLS + +/** + * PhoshCallState: + * + * The call state. Must match call's CallsCallState. + */ +typedef enum +{ + /*< private >*/ + PHOSH_CALL_STATE_ACTIVE = 1, + PHOSH_CALL_STATE_HELD = 2, + PHOSH_CALL_STATE_DIALING = 3, + /* PHOSH_CALL_STATE_ALERTING (deprecated) */ + PHOSH_CALL_STATE_INCOMING = 5, + /* PHOSH_CALL_STATE_WAITING (deprecated) */ + PHOSH_CALL_STATE_DISCONNECTED = 7, +} PhoshCallState; + + +#define PHOSH_TYPE_CALLS_MANAGER (phosh_calls_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshCallsManager, phosh_calls_manager, PHOSH, CALLS_MANAGER, PhoshManager) + +PhoshCallsManager *phosh_calls_manager_new (void); +gboolean phosh_calls_manager_get_present (PhoshCallsManager *self); +gboolean phosh_calls_manager_has_incoming_call (PhoshCallsManager *self); +const char *phosh_calls_manager_get_active_call_handle (PhoshCallsManager *self); +PhoshCall *phosh_calls_manager_get_call (PhoshCallsManager *self, const char *handle); + +G_END_DECLS diff --git a/src/cell-broadcast-manager.c b/src/cell-broadcast-manager.c new file mode 100644 index 000000000..df341a3ab --- /dev/null +++ b/src/cell-broadcast-manager.c @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-cell-broadcast-manager" + +#include "phosh-config.h" + +#include "cell-broadcast-manager.h" +#include "cell-broadcast-prompt.h" +#include "feedback-manager.h" +#include "shell-priv.h" +#include "wwan/wwan-manager.h" + +#include + +#define PHOSH_CELL_BROOADCAST_SCHEMA_ID "mobi.phosh.shell.cell-broadcast" + +/** + * PhoshCellBroadcastManager: + * + * Handles the display of Cell Broadcast messages + * + * Since: 0.44.0 + */ + +enum { + PROP_0, + PROP_ENABLED, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshCellBroadcastManager { + GObject parent; + + gboolean enabled; + GSettings *settings; +}; +G_DEFINE_TYPE (PhoshCellBroadcastManager, phosh_cell_broadcast_manager, G_TYPE_OBJECT) + + +static const char * +id_to_title (guint id) +{ + /* Translators: These strings aren't translated */ + switch (id) { + /* TODO: Mapping by country */ + case 4383: + case 4370: + return "National Emergency Alert"; + case 4371: + return "Emergency Alert"; + default: + return "Cell broadcast"; + } +} + + +static void +on_new_cbm (PhoshCellBroadcastManager *self, const char *message, guint id) +{ + GtkWidget *prompt; + + g_debug ("New cbm: %s", message); + + if (!self->enabled) + return; + + prompt = phosh_cell_broadcast_prompt_new (message, id_to_title (id)); + g_signal_connect (prompt, "closed", G_CALLBACK (gtk_widget_destroy), NULL); + gtk_widget_set_visible (prompt, TRUE); + phosh_trigger_feedback ("message-new-cellbroadcast"); + phosh_shell_activate_action (phosh_shell_get_default (), "screensaver.wakeup-screen", NULL); + + /* We rely on the Chat application to remove the CBM later on */ +} + + +static void +phosh_cell_broadcast_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCellBroadcastManager *self = PHOSH_CELL_BROADCAST_MANAGER (object); + + switch (property_id) { + case PROP_ENABLED: + self->enabled = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_cell_broadcast_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCellBroadcastManager *self = PHOSH_CELL_BROADCAST_MANAGER (object); + + switch (property_id) { + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_cell_broadcast_manager_finalize (GObject *object) +{ + PhoshCellBroadcastManager *self = PHOSH_CELL_BROADCAST_MANAGER (object); + + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_cell_broadcast_manager_parent_class)->finalize (object); +} + + +static void +phosh_cell_broadcast_manager_class_init (PhoshCellBroadcastManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_cell_broadcast_manager_get_property; + object_class->set_property = phosh_cell_broadcast_manager_set_property; + object_class->finalize = phosh_cell_broadcast_manager_finalize; + + /** + * PhoshCellbroadcastmanager:enabled: + * + * Whether display of cell broadcast messages is enabled + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_cell_broadcast_manager_init (PhoshCellBroadcastManager *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshWWan *wwan = phosh_shell_get_wwan (shell); + + self->settings = g_settings_new (PHOSH_CELL_BROOADCAST_SCHEMA_ID); + g_settings_bind (self->settings, "enabled", self, "enabled", G_SETTINGS_BIND_GET); + + g_signal_connect_object (wwan, "new-cbm", G_CALLBACK (on_new_cbm), self, G_CONNECT_SWAPPED); +} + + +PhoshCellBroadcastManager * +phosh_cell_broadcast_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_CELL_BROADCAST_MANAGER, NULL); +} diff --git a/src/cell-broadcast-manager.h b/src/cell-broadcast-manager.h new file mode 100644 index 000000000..54be67032 --- /dev/null +++ b/src/cell-broadcast-manager.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CELL_BROADCAST_MANAGER (phosh_cell_broadcast_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshCellBroadcastManager, phosh_cell_broadcast_manager, + PHOSH, CELL_BROADCAST_MANAGER, GObject) + +PhoshCellBroadcastManager *phosh_cell_broadcast_manager_new (void); + +G_END_DECLS diff --git a/src/cell-broadcast-prompt.c b/src/cell-broadcast-prompt.c new file mode 100644 index 000000000..256113ba2 --- /dev/null +++ b/src/cell-broadcast-prompt.c @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-cell-broadcast-prompt" + +#include "phosh-config.h" + +#include "cell-broadcast-prompt.h" + +#include + +/** + * PhoshCellBroadcastPrompt: + * + * A modal prompt to display cell broadcast messages + * + * The #PhoshCellBroadcastPrompt is used to show cell + * broadcast messages from the mobile network. + * + * Since it's about emergencies it can be shown above the + * lock screen. + */ + +enum { + PROP_0, + PROP_MESSAGE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +struct _PhoshCellBroadcastPrompt { + PhoshSystemModalDialog parent; + + GtkLabel *lbl_msg; + char *message; +}; +G_DEFINE_TYPE (PhoshCellBroadcastPrompt, phosh_cell_broadcast_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + + +static void +set_message (PhoshCellBroadcastPrompt *self, const char *message) +{ + g_return_if_fail (PHOSH_IS_CELL_BROADCAST_PROMPT (self)); + + if (!g_strcmp0 (self->message, message)) + return; + + g_clear_pointer (&self->message, g_free); + self->message = g_strdup (message); + + g_strstrip (self->message); + gtk_label_set_label (self->lbl_msg, self->message); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MESSAGE]); +} + + +static void +phosh_cell_broadcast_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshCellBroadcastPrompt *self = PHOSH_CELL_BROADCAST_PROMPT (obj); + + switch (prop_id) { + case PROP_MESSAGE: + set_message (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_cell_broadcast_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshCellBroadcastPrompt *self = PHOSH_CELL_BROADCAST_PROMPT (obj); + + switch (prop_id) { + case PROP_MESSAGE: + g_value_set_string (value, self->message ?: ""); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +on_ok_clicked (PhoshCellBroadcastPrompt *self) +{ + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +on_dialog_canceled (PhoshCellBroadcastPrompt *self) +{ + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +phosh_cell_broadcast_prompt_finalize (GObject *obj) +{ + PhoshCellBroadcastPrompt *self = PHOSH_CELL_BROADCAST_PROMPT (obj); + + g_free (self->message); + + G_OBJECT_CLASS (phosh_cell_broadcast_prompt_parent_class)->finalize (obj); +} + + +static void +phosh_cell_broadcast_prompt_class_init (PhoshCellBroadcastPromptClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_cell_broadcast_prompt_get_property; + object_class->set_property = phosh_cell_broadcast_prompt_set_property; + object_class->finalize = phosh_cell_broadcast_prompt_finalize; + + props[PROP_MESSAGE] = + g_param_spec_string ("message", "", "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/cell-broadcast-prompt.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshCellBroadcastPrompt, lbl_msg); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); + gtk_widget_class_bind_template_callback (widget_class, on_ok_clicked); + + gtk_widget_class_set_css_name (widget_class, "phosh-cell-broadcast-prompt"); +} + + +static void +phosh_cell_broadcast_prompt_init (PhoshCellBroadcastPrompt *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_cell_broadcast_prompt_new (const char *message, const char *title) +{ + return g_object_new (PHOSH_TYPE_CELL_BROADCAST_PROMPT, + "message", message, + "title", title, + NULL); +} diff --git a/src/cell-broadcast-prompt.h b/src/cell-broadcast-prompt.h new file mode 100644 index 000000000..d5599913e --- /dev/null +++ b/src/cell-broadcast-prompt.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "system-modal-dialog.h" + +#define PHOSH_TYPE_CELL_BROADCAST_PROMPT (phosh_cell_broadcast_prompt_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshCellBroadcastPrompt, phosh_cell_broadcast_prompt, PHOSH, + CELL_BROADCAST_PROMPT, PhoshSystemModalDialog) + +GtkWidget *phosh_cell_broadcast_prompt_new (const char *message, const char *title); diff --git a/src/clamp.c b/src/clamp.c new file mode 100644 index 000000000..85048efd3 --- /dev/null +++ b/src/clamp.c @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "clamp.h" + +/** + * PhoshClamp: + * + * A container limiting its natural size request + * + * This should not be confused with `HdyClamp`, which limits the size allocated + * to its child and adds a dynamic margin around it. `PhoshClamp` instead + * constraints the requested natural size, the child will still be allocated all + * that the clamp gets. + */ + +enum { + PROP_0, + PROP_NATURAL_SIZE, + + /* Overridden properties */ + PROP_ORIENTATION, + + LAST_PROP = PROP_NATURAL_SIZE + 1, +}; + +struct _PhoshClamp +{ + GtkBin parent_instance; + + gint natural_size; + + GtkOrientation orientation; +}; + +static GParamSpec *props[LAST_PROP]; + +G_DEFINE_TYPE_WITH_CODE (PhoshClamp, phosh_clamp, GTK_TYPE_BIN, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL)) + +static void +set_orientation (PhoshClamp *self, + GtkOrientation orientation) +{ + if (self->orientation == orientation) + return; + + self->orientation = orientation; + gtk_widget_queue_resize (GTK_WIDGET (self)); + g_object_notify (G_OBJECT (self), "orientation"); +} + +static void +phosh_clamp_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshClamp *self = PHOSH_CLAMP (object); + + switch (prop_id) { + case PROP_NATURAL_SIZE: + g_value_set_int (value, phosh_clamp_get_natural_size (self)); + break; + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +phosh_clamp_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshClamp *self = PHOSH_CLAMP (object); + + switch (prop_id) { + case PROP_NATURAL_SIZE: + phosh_clamp_set_natural_size (self, g_value_get_int (value)); + break; + case PROP_ORIENTATION: + set_orientation (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +/* This private method is prefixed by the call name because it will be a virtual + * method in GTK 4. + */ +static void +phosh_clamp_measure (GtkWidget *widget, + GtkOrientation orientation, + int for_size, + int *minimum, + int *natural, + int *minimum_baseline, + int *natural_baseline) +{ + PhoshClamp *self = PHOSH_CLAMP (widget); + GtkBin *bin = GTK_BIN (widget); + GtkWidget *child; + int child_min = 0; + int child_nat = 0; + int child_min_baseline = -1; + int child_nat_baseline = -1; + + if (minimum) + *minimum = 0; + if (natural) + *natural = 0; + if (minimum_baseline) + *minimum_baseline = -1; + if (natural_baseline) + *natural_baseline = -1; + + child = gtk_bin_get_child (bin); + + if (!child || !gtk_widget_is_visible (child)) + return; + + if (self->orientation == orientation) { + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width (child, &child_min, &child_nat); + else + gtk_widget_get_preferred_height_and_baseline_for_width (child, -1, + &child_min, + &child_nat, + &child_min_baseline, + &child_nat_baseline); + + child_nat = MIN (child_nat, self->natural_size); + child_nat = MAX (child_min, child_nat); + } else { + if (orientation == GTK_ORIENTATION_HORIZONTAL) + gtk_widget_get_preferred_width_for_height (child, for_size, + &child_min, &child_nat); + else + gtk_widget_get_preferred_height_and_baseline_for_width (child, for_size, + &child_min, + &child_nat, + &child_min_baseline, + &child_nat_baseline); + } + + if (minimum) + *minimum = child_min; + if (natural) + *natural = child_nat; + if (minimum_baseline && child_min_baseline > -1) + *minimum_baseline = child_min_baseline; + if (natural_baseline && child_nat_baseline > -1) + *natural_baseline = child_nat_baseline; +} + +static GtkSizeRequestMode +phosh_clamp_get_request_mode (GtkWidget *widget) +{ + PhoshClamp *self = PHOSH_CLAMP (widget); + + return self->orientation == GTK_ORIENTATION_HORIZONTAL ? + GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH : + GTK_SIZE_REQUEST_WIDTH_FOR_HEIGHT; +} + +static void +phosh_clamp_get_preferred_width_for_height (GtkWidget *widget, + gint height, + gint *minimum, + gint *natural) +{ + phosh_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, height, + minimum, natural, NULL, NULL); +} + +static void +phosh_clamp_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + phosh_clamp_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1, + minimum, natural, NULL, NULL); +} + +static void +phosh_clamp_get_preferred_height_and_baseline_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural, + gint *minimum_baseline, + gint *natural_baseline) +{ + phosh_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, minimum_baseline, natural_baseline); +} + +static void +phosh_clamp_get_preferred_height_for_width (GtkWidget *widget, + gint width, + gint *minimum, + gint *natural) +{ + phosh_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, width, + minimum, natural, NULL, NULL); +} + +static void +phosh_clamp_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + phosh_clamp_measure (widget, GTK_ORIENTATION_VERTICAL, -1, + minimum, natural, NULL, NULL); +} + +static void +phosh_clamp_class_init (PhoshClampClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->get_property = phosh_clamp_get_property; + object_class->set_property = phosh_clamp_set_property; + + widget_class->get_request_mode = phosh_clamp_get_request_mode; + widget_class->get_preferred_width = phosh_clamp_get_preferred_width; + widget_class->get_preferred_width_for_height = phosh_clamp_get_preferred_width_for_height; + widget_class->get_preferred_height = phosh_clamp_get_preferred_height; + widget_class->get_preferred_height_for_width = phosh_clamp_get_preferred_height_for_width; + widget_class->get_preferred_height_and_baseline_for_width = phosh_clamp_get_preferred_height_and_baseline_for_width; + + gtk_container_class_handle_border_width (container_class); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + /** + * PhoshClamp:natural-size: + * + * The maximum natural size request. + */ + props[PROP_NATURAL_SIZE] = + g_param_spec_int ("natural-size", "", "", + -1, G_MAXINT, -1, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "clamp"); +} + +static void +phosh_clamp_init (PhoshClamp *self) +{ + self->natural_size = -1; +} + +/** + * phosh_clamp_new: + * + * Creates a new #PhoshClamp. + * + * Returns: a new #PhoshClamp + */ +GtkWidget * +phosh_clamp_new (void) +{ + return g_object_new (PHOSH_TYPE_CLAMP, NULL); +} + +/** + * phosh_clamp_get_natural_size: + * @self: a #PhoshClamp + * + * Gets the maximum natural size request. + * + * Returns: the maximum natural size request. + */ +gint +phosh_clamp_get_natural_size (PhoshClamp *self) +{ + g_return_val_if_fail (PHOSH_IS_CLAMP (self), 0); + + return self->natural_size; +} + +/** + * phosh_clamp_set_natural_size: + * @self: a #PhoshClamp + * @natural_size: the maximum natural size + * + * Sets the maximum natural size request. + */ +void +phosh_clamp_set_natural_size (PhoshClamp *self, + gint natural_size) +{ + g_return_if_fail (PHOSH_IS_CLAMP (self)); + + if (self->natural_size == natural_size) + return; + + self->natural_size = natural_size; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NATURAL_SIZE]); +} diff --git a/src/clamp.h b/src/clamp.h new file mode 100644 index 000000000..7b1d00edf --- /dev/null +++ b/src/clamp.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CLAMP (phosh_clamp_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshClamp, phosh_clamp, PHOSH, CLAMP, GtkBin) + +GtkWidget *phosh_clamp_new (void); +gint phosh_clamp_get_natural_size (PhoshClamp *self); +void phosh_clamp_set_natural_size (PhoshClamp *self, + gint natural_size); + +G_END_DECLS diff --git a/src/connectivity-info.c b/src/connectivity-info.c new file mode 100644 index 000000000..78bd7674d --- /dev/null +++ b/src/connectivity-info.c @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-connectivity-info" + +#include "phosh-config.h" + +#include "connectivity-info.h" +#include "connectivity-manager.h" +#include "util.h" +#include "shell-priv.h" + +#include + +/** + * PhoshConnectivityInfo: + * + * A widget to display the connectivity status + * + * #PhoshConnectivityInfo displays the internet connection status as + * determined by #PhoshConnectivityManager and alerts the user about + * connectivity problems. + * + * Usually there's no point in showing the + * widget when #PhoshConnectivityInfo:connectivity is %TRUE but it's + * up to the container to decide. + */ + +enum { + PROP_0, + PROP_CONNECTIVITY, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshConnectivityInfo { + PhoshStatusIcon parent; + + gboolean connectivity; + PhoshConnectivityManager *connectivity_manager; +}; +G_DEFINE_TYPE (PhoshConnectivityInfo, phosh_connectivity_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_connectivity_info_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshConnectivityInfo *self = PHOSH_CONNECTIVITY_INFO (obj); + + switch (prop_id) { + case PROP_CONNECTIVITY: + self->connectivity = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_connectivity_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshConnectivityInfo *self = PHOSH_CONNECTIVITY_INFO (object); + + switch (property_id) { + case PROP_CONNECTIVITY: + g_value_set_boolean (value, self->connectivity); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_connectivity_info_constructed (GObject *object) +{ + PhoshConnectivityInfo *self = PHOSH_CONNECTIVITY_INFO (object); + PhoshConnectivityManager *manager; + + G_OBJECT_CLASS (phosh_connectivity_info_parent_class)->constructed (object); + + manager = phosh_shell_get_connectivity_manager (phosh_shell_get_default ()); + g_return_if_fail (PHOSH_IS_CONNECTIVITY_MANAGER (manager)); + + g_object_bind_property (manager, "connectivity", self, "connectivity", G_BINDING_SYNC_CREATE); + g_object_bind_property (manager, "icon-name", self, "icon-name", G_BINDING_SYNC_CREATE); +} + + +static void +phosh_connectivity_info_class_init (PhoshConnectivityInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_connectivity_info_constructed; + object_class->get_property = phosh_connectivity_info_get_property; + object_class->set_property = phosh_connectivity_info_set_property; + + gtk_widget_class_set_css_name (widget_class, "phosh-connectivity-info"); + + /* PhoshConnectivityInfo:connectivity: + * + * %TRUE if a connection to the internet is present, otherwise %FALSE + */ + props[PROP_CONNECTIVITY] = + g_param_spec_boolean ("connectivity", "", "", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_connectivity_info_init (PhoshConnectivityInfo *self) +{ +} + + +GtkWidget * +phosh_connectivity_info_new (void) +{ + return g_object_new (PHOSH_TYPE_CONNECTIVITY_INFO, NULL); +} diff --git a/src/connectivity-info.h b/src/connectivity-info.h new file mode 100644 index 000000000..efc2b7996 --- /dev/null +++ b/src/connectivity-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CONNECTIVITY_INFO (phosh_connectivity_info_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshConnectivityInfo, phosh_connectivity_info, PHOSH, CONNECTIVITY_INFO, PhoshStatusIcon) + +GtkWidget * phosh_connectivity_info_new (void); + +G_END_DECLS diff --git a/src/connectivity-manager.c b/src/connectivity-manager.c new file mode 100644 index 000000000..25bb720e1 --- /dev/null +++ b/src/connectivity-manager.c @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-connectivity-manager" + +#include "config.h" + +#include "connectivity-manager.h" +#include "notify-manager.h" +#include "shell-priv.h" +#include "util.h" + +#include + +#define NOTI_TIMEOUT (20 * 1000) + +/** + * PhoshConnectivityManager + * + * `PhoshConnectivityManager` monitors the connectivity to the + * internet via NetworkManager. + */ + +enum { + PROP_0, + PROP_CONNECTIVITY, + PROP_ICON_NAME, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshConnectivityManager { + PhoshManager parent; + + PhoshNotification *noti; + char *ssid; + + gboolean connectivity; + NMClient *nmclient; + GCancellable *cancel; + + char *icon_name; + NMConnectivityState state; +}; +G_DEFINE_TYPE (PhoshConnectivityManager, phosh_connectivity_manager, PHOSH_TYPE_MANAGER) + + +static void +on_notification_actioned (PhoshConnectivityManager *self) +{ + gboolean success; + const char *check_uri; + g_autoptr (GError) err = NULL; + + check_uri = nm_client_connectivity_check_get_uri (self->nmclient); + if (check_uri == NULL) { + g_warning ("No connectivity check URI configured"); + return; + } + + success = gtk_show_uri_on_window (NULL, check_uri, GDK_CURRENT_TIME, &err); + if (!success) + g_warning ("Failed to show uri '%s': %s", check_uri, err->message); +} + + +static void +on_notification_closed (PhoshConnectivityManager *self) +{ + g_clear_object (&self->noti); +} + + +static void +on_ssid_changed (PhoshConnectivityManager *self, GParamSpec *pspec, PhoshWifiManager *wifi_manager) +{ + const char *new_ssid = phosh_wifi_manager_get_ssid (wifi_manager); + + if (!self->noti) + return; + + if (g_strcmp0 (self->ssid, new_ssid) == 0) + return; + + g_debug ("SSID changed from '%s' to '%s', closing notification", self->ssid, new_ssid); + g_clear_pointer (&self->ssid, g_free); + phosh_notification_close (self->noti, PHOSH_NOTIFICATION_REASON_CLOSED); +} + + +static void +portal_auth (PhoshConnectivityManager *self) +{ + PhoshNotifyManager *nm = phosh_notify_manager_get_default (); + PhoshWifiManager *wifi_manager = phosh_shell_get_wifi_manager (phosh_shell_get_default ()); + g_autoptr (GIcon) icon = g_themed_icon_new ("network-wireless-signal-none-symbolic"); + g_autofree char *body = NULL; + const char *check_uri; + + check_uri = nm_client_connectivity_check_get_uri (self->nmclient); + if (check_uri == NULL) { + g_warning ("No connectivity check URI configured, ignoring"); + return; + } + + g_free (self->ssid); + self->ssid = g_strdup (phosh_wifi_manager_get_ssid (wifi_manager)); + if (self->ssid) + body = g_strdup_printf (_("Wi-Fi network '%s' uses a captive portal"), self->ssid); + else + body = g_strdup (_("The Wi-Fi network uses a captive portal")); + + if (self->noti) { + phosh_notification_set_body (self->noti, body); + phosh_notification_expires (self->noti, NOTI_TIMEOUT); + } else { + self->noti = g_object_new (PHOSH_TYPE_NOTIFICATION, + "summary", _("Sign into Wi-Fi network"), + "body", body, + "image", icon, + NULL); + g_object_connect (self->noti, + "swapped-object-signal::actioned", on_notification_actioned, self, + "swapped-object-signal::closed", on_notification_closed, self, + NULL); + phosh_notify_manager_add_shell_notification (nm, self->noti, 0, NOTI_TIMEOUT); + phosh_notification_set_transient (self->noti, FALSE); + phosh_notification_set_profile (self->noti, "silent"); + } +} + + +static void +phosh_connectivity_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshConnectivityManager *self = PHOSH_CONNECTIVITY_MANAGER (object); + + switch (property_id) { + case PROP_CONNECTIVITY: + g_value_set_boolean (value, self->connectivity); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +on_connectivity_changed (PhoshConnectivityManager *self, GParamSpec *pspec, NMClient *nmclient) +{ + const char *icon_name; + NMConnectivityState state; + gboolean connectivity = FALSE; + + g_return_if_fail (PHOSH_IS_CONNECTIVITY_MANAGER (self)); + g_return_if_fail (NM_IS_CLIENT (nmclient)); + + state = nm_client_get_connectivity (nmclient); + + if (self->state == state) + return; + + self->state = state; + + switch (state) { + case NM_CONNECTIVITY_NONE: + icon_name = "network-offline-symbolic"; + break; + case NM_CONNECTIVITY_LIMITED: + icon_name = "network-no-route-symbolic"; + break; + case NM_CONNECTIVITY_PORTAL: + portal_auth (self); + icon_name = "network-no-route-symbolic"; + break; + case NM_CONNECTIVITY_UNKNOWN: + case NM_CONNECTIVITY_FULL: + default: + icon_name = "network-transmit-receive-symbolic"; + connectivity = TRUE; + } + + g_debug ("Connectivity changed (%d), updating icon to '%s'", state, icon_name); + + if (connectivity != self->connectivity) { + self->connectivity = connectivity; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONNECTIVITY]); + } + + if (g_strcmp0 (self->icon_name, icon_name)) { + g_free (self->icon_name); + self->icon_name = g_strdup (icon_name); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + } + + /* If we get connectivity again, close the portal notification, no matter how + * that happened */ + if (connectivity && self->noti) { + phosh_notification_close (self->noti, PHOSH_NOTIFICATION_REASON_CLOSED); + } +} + + +static void +on_nm_client_ready (GObject *obj, GAsyncResult *res, gpointer user_data) +{ + PhoshConnectivityManager *self = PHOSH_CONNECTIVITY_MANAGER (user_data); + g_autoptr (GError) err = NULL; + NMClient *nmclient; + + nmclient = nm_client_new_finish (res, &err); + if (!nmclient) { + phosh_async_error_warn (err, "Failed to init NM"); + return; + } + + g_return_if_fail (PHOSH_IS_CONNECTIVITY_MANAGER (self)); + self->nmclient = nmclient; + + g_return_if_fail (NM_IS_CLIENT (self->nmclient)); + + g_signal_connect_swapped (self->nmclient, "notify::connectivity", + G_CALLBACK (on_connectivity_changed), self); + + on_connectivity_changed (self, NULL, self->nmclient); +} + + +static void +phosh_connectivity_manager_idle_init (PhoshManager *manager) +{ + PhoshConnectivityManager *self = PHOSH_CONNECTIVITY_MANAGER (manager); + + nm_client_new_async (self->cancel, on_nm_client_ready, self); +} + + +static void +phosh_connectivity_manager_finalize (GObject *object) +{ + PhoshConnectivityManager *self = PHOSH_CONNECTIVITY_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->nmclient); + g_clear_object (&self->noti); + g_clear_pointer (&self->ssid, g_free); + + G_OBJECT_CLASS (phosh_connectivity_manager_parent_class)->finalize (object); +} + + +static void +phosh_connectivity_manager_class_init (PhoshConnectivityManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->get_property = phosh_connectivity_manager_get_property; + object_class->finalize = phosh_connectivity_manager_finalize; + + manager_class->idle_init = phosh_connectivity_manager_idle_init; + + /** + * PhoshConnectivityManager:connectivity: + * + * Whether there is a connection to the internet + */ + props[PROP_CONNECTIVITY] = + g_param_spec_boolean ("connectivity", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_connectivity_manager_init (PhoshConnectivityManager *self) +{ + PhoshWifiManager *wifi_manager = phosh_shell_get_wifi_manager (phosh_shell_get_default ()); + + self->cancel = g_cancellable_new (); + /* Ensure initial sync */ + self->state = -1; + + g_signal_connect_object (wifi_manager, + "notify::ssid", + G_CALLBACK (on_ssid_changed), + self, + G_CONNECT_SWAPPED); +} + + +PhoshConnectivityManager * +phosh_connectivity_manager_new (void) +{ + return PHOSH_CONNECTIVITY_MANAGER (g_object_new (PHOSH_TYPE_CONNECTIVITY_MANAGER, NULL)); +} diff --git a/src/connectivity-manager.h b/src/connectivity-manager.h new file mode 100644 index 000000000..83d90ca8e --- /dev/null +++ b/src/connectivity-manager.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CONNECTIVITY_MANAGER (phosh_connectivity_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshConnectivityManager, phosh_connectivity_manager, PHOSH, + CONNECTIVITY_MANAGER, PhoshManager) + +PhoshConnectivityManager *phosh_connectivity_manager_new (void); + +G_END_DECLS diff --git a/src/contrib/gnome-bluetooth/bluetooth-client.h b/src/contrib/gnome-bluetooth/bluetooth-client.h new file mode 100644 index 000000000..c79c460dc --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-client.h @@ -0,0 +1,48 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2005-2008 Marcel Holtmann + * Copyright (C) 2009-2021 Red Hat Inc. + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include +#include + +#define BLUETOOTH_TYPE_CLIENT (bluetooth_client_get_type()) +G_DECLARE_FINAL_TYPE (BluetoothClient, bluetooth_client, BLUETOOTH, CLIENT, GObject) + +BluetoothClient *bluetooth_client_new(void); + +GListStore *bluetooth_client_get_devices (BluetoothClient *client); + +void bluetooth_client_connect_service (BluetoothClient *client, + const char *path, + gboolean connect, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); + +gboolean bluetooth_client_connect_service_finish (BluetoothClient *client, + GAsyncResult *res, + GError **error); + +gboolean bluetooth_client_has_connected_input_devices (BluetoothClient *client); diff --git a/src/contrib/gnome-bluetooth/bluetooth-device.h b/src/contrib/gnome-bluetooth/bluetooth-device.h new file mode 100644 index 000000000..82d8dc7f7 --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-device.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2021 Bastien Nocera + * + * SPDX-License-Identifier: LGPL-2.1-or-later + */ + +#pragma once + +#include +#include + +#define BLUETOOTH_TYPE_DEVICE (bluetooth_device_get_type()) +G_DECLARE_FINAL_TYPE (BluetoothDevice, bluetooth_device, BLUETOOTH, DEVICE, GObject) + +const char *bluetooth_device_get_object_path (BluetoothDevice *device); +void bluetooth_device_dump (BluetoothDevice *device); +char *bluetooth_device_to_string (BluetoothDevice *device); diff --git a/src/contrib/gnome-bluetooth/bluetooth-enums.h b/src/contrib/gnome-bluetooth/bluetooth-enums.h new file mode 100644 index 000000000..3c972d62f --- /dev/null +++ b/src/contrib/gnome-bluetooth/bluetooth-enums.h @@ -0,0 +1,132 @@ +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2005-2008 Marcel Holtmann + * + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#pragma once + +#include + +/** + * SECTION:bluetooth-enums + * @short_description: Bluetooth related enumerations + * @stability: Stable + * @include: bluetooth-enums.h + * + * Enumerations related to Bluetooth. + **/ + +/** + * BluetoothType: + * @BLUETOOTH_TYPE_ANY: any device, or a device of an unknown type + * @BLUETOOTH_TYPE_PHONE: a telephone (usually a cell/mobile phone) + * @BLUETOOTH_TYPE_MODEM: a modem + * @BLUETOOTH_TYPE_COMPUTER: a computer, can be a laptop, a wearable computer, etc. + * @BLUETOOTH_TYPE_NETWORK: a network device, such as a router + * @BLUETOOTH_TYPE_HEADSET: a headset (usually a hands-free device) + * @BLUETOOTH_TYPE_HEADPHONES: headphones (covers two ears) + * @BLUETOOTH_TYPE_OTHER_AUDIO: another type of audio device + * @BLUETOOTH_TYPE_KEYBOARD: a keyboard + * @BLUETOOTH_TYPE_MOUSE: a mouse + * @BLUETOOTH_TYPE_CAMERA: a camera (still or moving) + * @BLUETOOTH_TYPE_PRINTER: a printer + * @BLUETOOTH_TYPE_JOYPAD: a joypad, joystick, or other game controller + * @BLUETOOTH_TYPE_TABLET: a drawing tablet + * @BLUETOOTH_TYPE_VIDEO: a video device, such as a webcam + * @BLUETOOTH_TYPE_REMOTE_CONTROL: a remote control + * @BLUETOOTH_TYPE_SCANNER: a scanner + * @BLUETOOTH_TYPE_DISPLAY: a display + * @BLUETOOTH_TYPE_WEARABLE: a wearable computer + * @BLUETOOTH_TYPE_TOY: a toy or game + * @BLUETOOTH_TYPE_SPEAKERS: audio speaker or speakers + * + * The type of a Bluetooth device. See also %BLUETOOTH_TYPE_INPUT and %BLUETOOTH_TYPE_AUDIO + **/ +typedef enum { + BLUETOOTH_TYPE_ANY = 1 << 0, + BLUETOOTH_TYPE_PHONE = 1 << 1, + BLUETOOTH_TYPE_MODEM = 1 << 2, + BLUETOOTH_TYPE_COMPUTER = 1 << 3, + BLUETOOTH_TYPE_NETWORK = 1 << 4, + BLUETOOTH_TYPE_HEADSET = 1 << 5, + BLUETOOTH_TYPE_HEADPHONES = 1 << 6, + BLUETOOTH_TYPE_OTHER_AUDIO = 1 << 7, + BLUETOOTH_TYPE_KEYBOARD = 1 << 8, + BLUETOOTH_TYPE_MOUSE = 1 << 9, + BLUETOOTH_TYPE_CAMERA = 1 << 10, + BLUETOOTH_TYPE_PRINTER = 1 << 11, + BLUETOOTH_TYPE_JOYPAD = 1 << 12, + BLUETOOTH_TYPE_TABLET = 1 << 13, + BLUETOOTH_TYPE_VIDEO = 1 << 14, + BLUETOOTH_TYPE_REMOTE_CONTROL = 1 << 15, + BLUETOOTH_TYPE_SCANNER = 1 << 16, + BLUETOOTH_TYPE_DISPLAY = 1 << 17, + BLUETOOTH_TYPE_WEARABLE = 1 << 18, + BLUETOOTH_TYPE_TOY = 1 << 19, + BLUETOOTH_TYPE_SPEAKERS = 1 << 20, +} BluetoothType; + +#define _BLUETOOTH_TYPE_NUM_TYPES 21 + +/** + * BLUETOOTH_TYPE_INPUT: + * + * Use this value to select any Bluetooth input device where a #BluetoothType enum is required. + */ +#define BLUETOOTH_TYPE_INPUT (BLUETOOTH_TYPE_KEYBOARD | BLUETOOTH_TYPE_MOUSE | BLUETOOTH_TYPE_TABLET | BLUETOOTH_TYPE_JOYPAD) +/** + * BLUETOOTH_TYPE_AUDIO: + * + * Use this value to select any Bluetooth audio device where a #BluetoothType enum is required. + */ +#define BLUETOOTH_TYPE_AUDIO (BLUETOOTH_TYPE_HEADSET | BLUETOOTH_TYPE_HEADPHONES | BLUETOOTH_TYPE_OTHER_AUDIO | BLUETOOTH_TYPE_SPEAKERS) + +/** + * BluetoothBatteryType: + * @BLUETOOTH_BATTERY_TYPE_NONE: no battery reporting + * @BLUETOOTH_BATTERY_TYPE_PERCENTAGE: battery reported in percentage + * @BLUETOOTH_BATTERY_TYPE_COARSE: battery reported coarsely + * + * The type of battery reporting supported by the device. + **/ +typedef enum { + BLUETOOTH_BATTERY_TYPE_NONE, + BLUETOOTH_BATTERY_TYPE_PERCENTAGE, + BLUETOOTH_BATTERY_TYPE_COARSE +} BluetoothBatteryType; + +/** + * BluetoothAdapterState: + * @BLUETOOTH_ADAPTER_STATE_ABSENT: Bluetooth adapter is missing. + * @BLUETOOTH_ADAPTER_STATE_ON: Bluetooth adapter is on. + * @BLUETOOTH_ADAPTER_STATE_TURNING_ON: Bluetooth adapter is being turned on. + * @BLUETOOTH_ADAPTER_STATE_TURNING_OFF: Bluetooth adapter is being turned off. + * @BLUETOOTH_ADAPTER_STATE_OFF: Bluetooth adapter is off. + * + * A more precise power state for a Bluetooth adapter. + **/ +typedef enum { + BLUETOOTH_ADAPTER_STATE_ABSENT = 0, + BLUETOOTH_ADAPTER_STATE_ON, + BLUETOOTH_ADAPTER_STATE_TURNING_ON, + BLUETOOTH_ADAPTER_STATE_TURNING_OFF, + BLUETOOTH_ADAPTER_STATE_OFF, +} BluetoothAdapterState; diff --git a/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h b/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h new file mode 100644 index 000000000..c0616e07e --- /dev/null +++ b/src/contrib/gnome-bluetooth/gnome-bluetooth-enum-types.h @@ -0,0 +1,28 @@ + +/* This file is generated by glib-mkenums, do not modify it. This code is licensed under the same license as the containing project. Note that it links to GLib, so must comply with the LGPL linking clauses. */ + +#pragma once + + #include + + + G_BEGIN_DECLS + +/* enumerations from "bluetooth-enums.h" */ + + +GType bluetooth_type_get_type (void); +#define BLUETOOTH_TYPE_TYPE (bluetooth_type_get_type()) + + +GType bluetooth_battery_type_get_type (void); +#define BLUETOOTH_TYPE_BATTERY_TYPE (bluetooth_battery_type_get_type()) + + +GType bluetooth_adapter_state_get_type (void); +#define BLUETOOTH_TYPE_ADAPTER_STATE (bluetooth_adapter_state_get_type()) + +G_END_DECLS + +/* Generated data ends here */ + diff --git a/src/contrib/shell-network-agent.c b/src/contrib/shell-network-agent.c new file mode 100644 index 000000000..ff3568b9a --- /dev/null +++ b/src/contrib/shell-network-agent.c @@ -0,0 +1,899 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2011 Red Hat, Inc. + * 2011 Giovanni Campagna + * 2017 Lubomir Rintel + * + * 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, Fifth Floor, Boston, MA 02110-1301 USA. + * + */ + +#include "phosh-config.h" +#include + +#include + +#include "shell-network-agent.h" + +enum { + SIGNAL_NEW_REQUEST, + SIGNAL_CANCEL_REQUEST, + SIGNAL_LAST +}; + +static int signals[SIGNAL_LAST]; + +typedef struct { + GCancellable * cancellable; + ShellNetworkAgent *self; + + char *request_id; + NMConnection *connection; + char *setting_name; + char **hints; + NMSecretAgentGetSecretsFlags flags; + NMSecretAgentOldGetSecretsFunc callback; + gpointer callback_data; + + GVariantDict *entries; + GVariantBuilder builder_vpn; +} ShellAgentRequest; + +struct _ShellNetworkAgentPrivate { + /* */ + GHashTable *requests; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellNetworkAgent, shell_network_agent, NM_TYPE_SECRET_AGENT_OLD) + +static const SecretSchema network_agent_schema = { + .name = "org.freedesktop.NetworkManager.Connection", + .flags = SECRET_SCHEMA_DONT_MATCH_NAME, + .attributes = { + { SHELL_KEYRING_UUID_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { SHELL_KEYRING_SN_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { SHELL_KEYRING_SK_TAG, SECRET_SCHEMA_ATTRIBUTE_STRING }, + { NULL, 0 }, + } +}; + +static void +shell_agent_request_free (gpointer data) +{ + ShellAgentRequest *request = data; + + g_cancellable_cancel (request->cancellable); + g_object_unref (request->cancellable); + g_object_unref (request->self); + g_object_unref (request->connection); + g_free (request->setting_name); + g_strfreev (request->hints); + g_clear_pointer (&request->entries, g_variant_dict_unref); + g_variant_builder_clear (&request->builder_vpn); + + g_slice_free (ShellAgentRequest, request); +} + +static void +shell_agent_request_cancel (ShellAgentRequest *request) +{ + GError *error; + ShellNetworkAgent *self; + + self = request->self; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "Canceled by NetworkManager"); + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + NULL, error, request->callback_data); + + g_signal_emit (self, signals[SIGNAL_CANCEL_REQUEST], 0, request->request_id); + + g_hash_table_remove (self->priv->requests, request->request_id); + g_error_free (error); +} + +static void +shell_network_agent_init (ShellNetworkAgent *agent) +{ + ShellNetworkAgentPrivate *priv; + + priv = agent->priv = shell_network_agent_get_instance_private (agent); + priv->requests = g_hash_table_new_full (g_str_hash, g_str_equal, + g_free, shell_agent_request_free); +} + +static void +shell_network_agent_finalize (GObject *object) +{ + ShellNetworkAgentPrivate *priv = SHELL_NETWORK_AGENT (object)->priv; + GError *error; + GHashTableIter iter; + gpointer key; + gpointer value; + + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_AGENT_CANCELED, + "The secret agent is going away"); + + g_hash_table_iter_init (&iter, priv->requests); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellAgentRequest *request = value; + + request->callback (NM_SECRET_AGENT_OLD (object), + request->connection, + NULL, error, + request->callback_data); + } + + g_hash_table_destroy (priv->requests); + g_error_free (error); + + G_OBJECT_CLASS (shell_network_agent_parent_class)->finalize (object); +} + +static void +request_secrets_from_ui (ShellAgentRequest *request) +{ + g_signal_emit (request->self, signals[SIGNAL_NEW_REQUEST], 0, + request->request_id, + request->connection, + request->setting_name, + request->hints, + (int)request->flags); +} + +static void +check_always_ask_cb (NMSetting *setting, + const char *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + gboolean *always_ask = user_data; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + if (flags & NM_SETTING_PARAM_SECRET) + { + if (nm_setting_get_secret_flags (setting, key, &secret_flags, NULL)) + { + if (secret_flags & NM_SETTING_SECRET_FLAG_NOT_SAVED) + *always_ask = TRUE; + } + } +} + +static gboolean +has_always_ask (NMSetting *setting) +{ + gboolean always_ask = FALSE; + + nm_setting_enumerate_values (setting, check_always_ask_cb, &always_ask); + return always_ask; +} + +static gboolean +is_connection_always_ask (NMConnection *connection) +{ + NMSettingConnection *s_con; + const char *ctype; + NMSetting *setting; + + /* For the given connection type, check if the secrets for that connection + * are always-ask or not. + */ + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + ctype = nm_setting_connection_get_connection_type (s_con); + + setting = nm_connection_get_setting_by_name (connection, ctype); + g_return_val_if_fail (setting != NULL, FALSE); + + if (has_always_ask (setting)) + return TRUE; + + /* Try type-specific settings too; be a bit paranoid and only consider + * secrets from settings relevant to the connection type. + */ + if (NM_IS_SETTING_WIRELESS (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_WIRELESS_SECURITY); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + else if (NM_IS_SETTING_WIRED (setting)) + { + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_PPPOE); + if (setting && has_always_ask (setting)) + return TRUE; + setting = nm_connection_get_setting (connection, NM_TYPE_SETTING_802_1X); + if (setting && has_always_ask (setting)) + return TRUE; + } + + return FALSE; +} + +static void +get_secrets_keyring_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + ShellAgentRequest *closure; + ShellNetworkAgent *self; + ShellNetworkAgentPrivate *priv; + GError *secret_error = NULL; + GError *error = NULL; + GList *items; + GList *l; + gboolean secrets_found = FALSE; + GVariantBuilder builder_setting, builder_connection; + GVariant *setting; + + items = secret_service_search_finish (NULL, result, &secret_error); + + if (g_error_matches (secret_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_error_free (secret_error); + return; + } + + closure = user_data; + self = closure->self; + priv = self->priv; + + if (secret_error != NULL) + { + g_set_error (&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Internal error while retrieving secrets from the keyring (%s)", secret_error->message); + g_error_free (secret_error); + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, NULL, error, closure->callback_data); + + goto out; + } + + g_variant_builder_init (&builder_setting, NM_VARIANT_TYPE_SETTING); + + for (l = items; l; l = g_list_next (l)) + { + SecretItem *item = l->data; + GHashTable *attributes; + GHashTableIter iter; + const char *name, *attribute; + SecretValue *secret = secret_item_get_secret (item); + + /* This can happen if the user denied a request to unlock */ + if (secret == NULL) + continue; + + attributes = secret_item_get_attributes (item); + g_hash_table_iter_init (&iter, attributes); + while (g_hash_table_iter_next (&iter, (gpointer *)&name, (gpointer *)&attribute)) + { + if (g_strcmp0 (name, SHELL_KEYRING_SK_TAG) == 0) + { + g_variant_builder_add (&builder_setting, "{sv}", attribute, + g_variant_new_string (secret_value_get (secret, NULL))); + + secrets_found = TRUE; + + break; + } + } + + g_hash_table_unref (attributes); + secret_value_unref (secret); + } + + g_list_free_full (items, g_object_unref); + setting = g_variant_builder_end (&builder_setting); + + /* All VPN requests get sent to the VPN's auth dialog, since it knows better + * than the agent about what secrets are required. Otherwise, if no secrets + * were found and interaction is allowed the ask for some secrets, because + * NetworkManager will fail the connection if not secrets are returned + * instead of asking again with REQUEST_NEW. + */ + if (strcmp(closure->setting_name, NM_SETTING_VPN_SETTING_NAME) == 0 || + (!secrets_found && (closure->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION))) + { + nm_connection_update_secrets (closure->connection, closure->setting_name, + setting, NULL); + + closure->entries = g_variant_dict_new (setting); + request_secrets_from_ui (closure); + return; + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + closure->setting_name, setting); + + closure->callback (NM_SECRET_AGENT_OLD (closure->self), closure->connection, + g_variant_builder_end (&builder_connection), NULL, + closure->callback_data); + + out: + g_hash_table_remove (priv->requests, closure->request_id); + g_clear_error (&error); +} + +static void +shell_network_agent_get_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const char *connection_path, + const char *setting_name, + const char **hints, + NMSecretAgentGetSecretsFlags flags, + NMSecretAgentOldGetSecretsFunc callback, + gpointer callback_data) +{ + ShellNetworkAgent *self = SHELL_NETWORK_AGENT (agent); + ShellAgentRequest *request; + GHashTable *attributes; + char *request_id; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + if ((request = g_hash_table_lookup (self->priv->requests, request_id)) != NULL) + { + /* We already have a request pending for this (connection, setting) + * Cancel it before starting the new one. + * This will also free the request structure and associated resources. + */ + shell_agent_request_cancel (request); + } + + request = g_slice_new0 (ShellAgentRequest); + request->self = g_object_ref (self); + request->cancellable = g_cancellable_new (); + request->connection = g_object_ref (connection); + request->setting_name = g_strdup (setting_name); + request->hints = g_strdupv ((char **) hints); + request->flags = flags; + request->callback = callback; + request->callback_data = callback_data; + + request->request_id = request_id; + g_hash_table_replace (self->priv->requests, request->request_id, request); + + g_variant_builder_init (&request->builder_vpn, G_VARIANT_TYPE ("a{ss}")); + + if ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) || + ((flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) + && is_connection_always_ask (request->connection))) + { + request->entries = g_variant_dict_new (NULL); + request_secrets_from_ui (request); + return; + } + + attributes = secret_attributes_build (&network_agent_schema, + SHELL_KEYRING_UUID_TAG, nm_connection_get_uuid (connection), + SHELL_KEYRING_SN_TAG, setting_name, + NULL); + + secret_service_search (NULL, &network_agent_schema, attributes, + SECRET_SEARCH_ALL | SECRET_SEARCH_UNLOCK | SECRET_SEARCH_LOAD_SECRETS, + request->cancellable, get_secrets_keyring_cb, request); + + g_hash_table_unref (attributes); +} + +void +shell_network_agent_add_vpn_secret (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_builder_add (&request->builder_vpn, "{ss}", setting_key, setting_value); +} + +void +shell_network_agent_set_password (ShellNetworkAgent *self, + char *request_id, + char *setting_key, + char *setting_value) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + g_variant_dict_insert (request->entries, setting_key, "s", setting_value); +} + +void +shell_network_agent_respond (ShellNetworkAgent *self, + char *request_id, + ShellNetworkAgentResponse response) +{ + ShellNetworkAgentPrivate *priv; + ShellAgentRequest *request; + GVariantBuilder builder_connection; + GVariant *vpn_secrets, *setting; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + + priv = self->priv; + request = g_hash_table_lookup (priv->requests, request_id); + g_return_if_fail (request != NULL); + + if (response == SHELL_NETWORK_AGENT_USER_CANCELED) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_USER_CANCELED, + "Network dialog was canceled by the user"); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + if (response == SHELL_NETWORK_AGENT_INTERNAL_ERROR) + { + GError *error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "An internal error occurred while processing the request."); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, NULL, error, request->callback_data); + g_error_free (error); + g_hash_table_remove (priv->requests, request_id); + return; + } + + /* response == SHELL_NETWORK_AGENT_CONFIRMED */ + + /* VPN secrets are stored as a hash of secrets in a single setting */ + vpn_secrets = g_variant_builder_end (&request->builder_vpn); + if (g_variant_n_children (vpn_secrets)) + g_variant_dict_insert_value (request->entries, NM_SETTING_VPN_SECRETS, vpn_secrets); + else + g_variant_unref (vpn_secrets); + + setting = g_variant_dict_end (request->entries); + + /* Save any updated secrets */ + if ((request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) || + (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW)) + { + NMConnection *dup = nm_simple_connection_new_clone (request->connection); + + nm_connection_update_secrets (dup, request->setting_name, setting, NULL); + nm_secret_agent_old_save_secrets (NM_SECRET_AGENT_OLD (self), dup, NULL, NULL); + g_object_unref (dup); + } + + g_variant_builder_init (&builder_connection, NM_VARIANT_TYPE_CONNECTION); + g_variant_builder_add (&builder_connection, "{s@a{sv}}", + request->setting_name, setting); + + request->callback (NM_SECRET_AGENT_OLD (self), request->connection, + g_variant_builder_end (&builder_connection), NULL, + request->callback_data); + + g_hash_table_remove (priv->requests, request_id); +} + +static void +search_vpn_plugin (GTask *task, + gpointer object, + gpointer task_data, + GCancellable *cancellable) +{ + NMVpnPluginInfo *info = NULL; + char *service = task_data; + + info = nm_vpn_plugin_info_new_search_file (NULL, service); + + if (info) + { + g_task_return_pointer (task, info, g_object_unref); + } + else + { + g_task_return_new_error (task, + G_IO_ERROR, G_IO_ERROR_NOT_FOUND, + "No plugin for %s", service); + } +} + +void +shell_network_agent_search_vpn_plugin (ShellNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_return_if_fail (SHELL_IS_NETWORK_AGENT (self)); + g_return_if_fail (service != NULL); + + task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (task, shell_network_agent_search_vpn_plugin); + g_task_set_task_data (task, g_strdup (service), g_free); + + g_task_run_in_thread (task, search_vpn_plugin); +} + +/** + * shell_network_agent_search_vpn_plugin_finish: + * + * Returns: (nullable) (transfer full): The found plugin or %NULL + */ +NMVpnPluginInfo * +shell_network_agent_search_vpn_plugin_finish (ShellNetworkAgent *self, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (SHELL_IS_NETWORK_AGENT (self), NULL); + g_return_val_if_fail (G_IS_TASK (result), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +shell_network_agent_cancel_get_secrets (NMSecretAgentOld *agent, + const char *connection_path, + const char *setting_name) +{ + ShellNetworkAgent *self = SHELL_NETWORK_AGENT (agent); + ShellNetworkAgentPrivate *priv = self->priv; + char *request_id; + ShellAgentRequest *request; + + request_id = g_strdup_printf ("%s/%s", connection_path, setting_name); + request = g_hash_table_lookup (priv->requests, request_id); + g_free (request_id); + + if (!request) + { + /* We've already sent the result, but the caller cancelled the + * operation before receiving that result. + */ + return; + } + + shell_agent_request_cancel (request); +} + +/************************* saving of secrets ****************************************/ + +static GHashTable * +create_keyring_add_attr_list (NMConnection *connection, + const char *connection_uuid, + const char *connection_id, + const char *setting_name, + const char *setting_key, + char **out_display_name) +{ + NMSettingConnection *s_con; + + if (connection) + { + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_return_val_if_fail (s_con != NULL, NULL); + connection_uuid = nm_setting_connection_get_uuid (s_con); + connection_id = nm_setting_connection_get_id (s_con); + } + + g_return_val_if_fail (connection_uuid != NULL, NULL); + g_return_val_if_fail (connection_id != NULL, NULL); + g_return_val_if_fail (setting_name != NULL, NULL); + g_return_val_if_fail (setting_key != NULL, NULL); + + if (out_display_name) + { + *out_display_name = g_strdup_printf ("Network secret for %s/%s/%s", + connection_id, + setting_name, + setting_key); + } + + return secret_attributes_build (&network_agent_schema, + SHELL_KEYRING_UUID_TAG, connection_uuid, + SHELL_KEYRING_SN_TAG, setting_name, + SHELL_KEYRING_SK_TAG, setting_key, + NULL); +} + +typedef struct +{ + /* Sort of ref count, indicates the number of secrets we still need to save */ + int n_secrets; + + NMSecretAgentOld *self; + NMConnection *connection; + gpointer callback; + gpointer callback_data; +} KeyringRequest; + +static void +keyring_request_free (KeyringRequest *r) +{ + g_object_unref (r->self); + g_object_unref (r->connection); + + g_slice_free (KeyringRequest, r); +} + +static void +save_secret_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *call = user_data; + NMSecretAgentOldSaveSecretsFunc callback = call->callback; + + call->n_secrets--; + + if (call->n_secrets == 0) + { + if (callback) + callback (call->self, call->connection, NULL, call->callback_data); + keyring_request_free (call); + } +} + +static void +save_one_secret (KeyringRequest *r, + NMSetting *setting, + const char *key, + const char *secret, + const char *display_name) +{ + GHashTable *attrs; + char *alt_display_name = NULL; + const char *setting_name; + NMSettingSecretFlags secret_flags = NM_SETTING_SECRET_FLAG_NONE; + + /* Only save agent-owned secrets (not system-owned or always-ask) */ + nm_setting_get_secret_flags (setting, key, &secret_flags, NULL); + if (secret_flags != NM_SETTING_SECRET_FLAG_AGENT_OWNED) + return; + + setting_name = nm_setting_get_name (setting); + g_assert (setting_name); + + attrs = create_keyring_add_attr_list (r->connection, NULL, NULL, + setting_name, + key, + display_name ? NULL : &alt_display_name); + g_assert (attrs); + r->n_secrets++; + secret_password_storev (&network_agent_schema, attrs, SECRET_COLLECTION_DEFAULT, + display_name ? display_name : alt_display_name, + secret, NULL, save_secret_cb, r); + + g_hash_table_unref (attrs); + g_free (alt_display_name); +} + +static void +vpn_secret_iter_cb (const char *key, + const char *secret, + gpointer user_data) +{ + KeyringRequest *r = user_data; + NMSetting *setting; + const char *service_name, *id; + char *display_name; + + if (secret && strlen (secret)) + { + setting = nm_connection_get_setting (r->connection, NM_TYPE_SETTING_VPN); + g_assert (setting); + service_name = nm_setting_vpn_get_service_type (NM_SETTING_VPN (setting)); + g_assert (service_name); + id = nm_connection_get_id (r->connection); + g_assert (id); + + display_name = g_strdup_printf ("VPN %s secret for %s/%s/" NM_SETTING_VPN_SETTING_NAME, + key, + id, + service_name); + save_one_secret (r, setting, key, secret, display_name); + g_free (display_name); + } +} + +static void +write_one_secret_to_keyring (NMSetting *setting, + const char *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + KeyringRequest *r = user_data; + const char *secret; + + /* Non-secrets obviously don't get saved in the keyring */ + if (!(flags & NM_SETTING_PARAM_SECRET)) + return; + + if (NM_IS_SETTING_VPN (setting) && (g_strcmp0 (key, NM_SETTING_VPN_SECRETS) == 0)) + { + /* Process VPN secrets specially since it's a hash of secrets, not just one */ + nm_setting_vpn_foreach_secret (NM_SETTING_VPN (setting), + vpn_secret_iter_cb, + r); + } + else + { + if (!G_VALUE_HOLDS_STRING (value)) + return; + + secret = g_value_get_string (value); + if (secret && strlen (secret)) + save_one_secret (r, setting, key, secret, NULL); + } +} + +static void +save_delete_cb (NMSecretAgentOld *agent, + NMConnection *connection, + GError *error, + gpointer user_data) +{ + KeyringRequest *r = user_data; + + /* Ignore errors; now save all new secrets */ + nm_connection_for_each_setting_value (connection, write_one_secret_to_keyring, r); + + /* If no secrets actually got saved there may be nothing to do so + * try to complete the request here. If there were secrets to save the + * request will get completed when those keyring calls return (at the next + * mainloop iteration). + */ + if (r->n_secrets == 0) + { + if (r->callback) + ((NMSecretAgentOldSaveSecretsFunc)r->callback) (agent, connection, NULL, r->callback_data); + keyring_request_free (r); + } +} + +static void +shell_network_agent_save_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const char *connection_path, + NMSecretAgentOldSaveSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + + r = g_slice_new (KeyringRequest); + r->n_secrets = 0; + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + /* First delete any existing items in the keyring */ + nm_secret_agent_old_delete_secrets (agent, connection, save_delete_cb, r); +} + +static void +delete_items_cb (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + KeyringRequest *r = user_data; + GError *secret_error = NULL; + GError *error = NULL; + NMSecretAgentOldDeleteSecretsFunc callback = r->callback; + + secret_password_clear_finish (result, &secret_error); + if (secret_error != NULL) + { + error = g_error_new (NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "The request could not be completed. Keyring result: %s", + secret_error->message); + g_error_free (secret_error); + } + + callback (r->self, r->connection, error, r->callback_data); + g_clear_error (&error); + keyring_request_free (r); +} + +static void +shell_network_agent_delete_secrets (NMSecretAgentOld *agent, + NMConnection *connection, + const char *connection_path, + NMSecretAgentOldDeleteSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + NMSettingConnection *s_con; + const char *uuid; + + r = g_slice_new (KeyringRequest); + r->n_secrets = 0; /* ignored by delete secrets calls */ + r->self = g_object_ref (agent); + r->connection = g_object_ref (connection); + r->callback = callback; + r->callback_data = callback_data; + + s_con = (NMSettingConnection *) nm_connection_get_setting (connection, NM_TYPE_SETTING_CONNECTION); + g_assert (s_con); + uuid = nm_setting_connection_get_uuid (s_con); + g_assert (uuid); + + secret_password_clear (&network_agent_schema, NULL, delete_items_cb, r, + SHELL_KEYRING_UUID_TAG, uuid, + NULL); +} + +void +shell_network_agent_class_init (ShellNetworkAgentClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + NMSecretAgentOldClass *agent_class = NM_SECRET_AGENT_OLD_CLASS (klass); + + gobject_class->finalize = shell_network_agent_finalize; + + agent_class->get_secrets = shell_network_agent_get_secrets; + agent_class->cancel_get_secrets = shell_network_agent_cancel_get_secrets; + agent_class->save_secrets = shell_network_agent_save_secrets; + agent_class->delete_secrets = shell_network_agent_delete_secrets; + + signals[SIGNAL_NEW_REQUEST] = g_signal_new ("new-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, /* return */ + 5, /* n_params */ + G_TYPE_STRING, + NM_TYPE_CONNECTION, + G_TYPE_STRING, + G_TYPE_STRV, + G_TYPE_INT); + + signals[SIGNAL_CANCEL_REQUEST] = g_signal_new ("cancel-request", + G_TYPE_FROM_CLASS (klass), + 0, /* flags */ + 0, /* class offset */ + NULL, /* accumulator */ + NULL, /* accu_data */ + NULL, /* marshaller */ + G_TYPE_NONE, + 1, /* n_params */ + G_TYPE_STRING); +} diff --git a/src/contrib/shell-network-agent.h b/src/contrib/shell-network-agent.h new file mode 100644 index 000000000..a93dc70cd --- /dev/null +++ b/src/contrib/shell-network-agent.h @@ -0,0 +1,77 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_NETWORK_AGENT_H__ +#define __SHELL_NETWORK_AGENT_H__ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +/** + * ShellNetworkAgentResponse: + */ +typedef enum { + /**/ + SHELL_NETWORK_AGENT_CONFIRMED, + SHELL_NETWORK_AGENT_USER_CANCELED, + SHELL_NETWORK_AGENT_INTERNAL_ERROR +} ShellNetworkAgentResponse; + +typedef struct _ShellNetworkAgent ShellNetworkAgent; +typedef struct _ShellNetworkAgentClass ShellNetworkAgentClass; +typedef struct _ShellNetworkAgentPrivate ShellNetworkAgentPrivate; + +#define SHELL_TYPE_NETWORK_AGENT (shell_network_agent_get_type ()) +#define SHELL_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgent)) +#define SHELL_IS_NETWORK_AGENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_NETWORK_AGENT)) +#define SHELL_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgentClass)) +#define SHELL_IS_NETWORK_AGENT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_NETWORK_AGENT)) +#define SHELL_NETWORK_AGENT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_NETWORK_AGENT, ShellNetworkAgentClass)) + +struct _ShellNetworkAgent +{ + /*< private >*/ + NMSecretAgentOld parent_instance; + + ShellNetworkAgentPrivate *priv; +}; + +struct _ShellNetworkAgentClass +{ + /*< private >*/ + NMSecretAgentOldClass parent_class; +}; + +/* used by SHELL_TYPE_NETWORK_AGENT */ +GType shell_network_agent_get_type (void); + +void shell_network_agent_add_vpn_secret (ShellNetworkAgent *self, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void shell_network_agent_set_password (ShellNetworkAgent *self, + char *request_id, + char *setting_key, + char *setting_value); +void shell_network_agent_respond (ShellNetworkAgent *self, + char *request_id, + ShellNetworkAgentResponse response); + +void shell_network_agent_search_vpn_plugin (ShellNetworkAgent *self, + const char *service, + GAsyncReadyCallback callback, + gpointer user_data); +NMVpnPluginInfo *shell_network_agent_search_vpn_plugin_finish (ShellNetworkAgent *self, + GAsyncResult *result, + GError **error); + +/* If these are kept in sync with nm-applet, secrets will be shared */ +#define SHELL_KEYRING_UUID_TAG "connection-uuid" +#define SHELL_KEYRING_SN_TAG "setting-name" +#define SHELL_KEYRING_SK_TAG "setting-key" + +G_END_DECLS + +#endif /* __SHELL_NETWORK_AGENT_H__ */ diff --git a/src/dbus/meson.build b/src/dbus/meson.build new file mode 100644 index 000000000..ef97d0075 --- /dev/null +++ b/src/dbus/meson.build @@ -0,0 +1,238 @@ +dbus_inc = include_directories('.') + +# DBus client interfaces +generated_dbus_sources = [] +generated_dbus_headers = [] +libphosh_generated_dbus_headers = [] +libphosh_generated_dbus_sources = [] + +dbus_prefix = 'PhoshDBus' +dbus_inc_dir = lib_inc_dir / 'dbus' + +# +# Protocols where Phosh is the DBus client: +# +dbus_client_protos = [ + # Sorted by xml filename: + [ + 'iio-sensor-proxy-dbus', + 'net.hadess.SensorProxy.xml', + 'net.hadess', + false, + false, + ], + [ + 'accounts-user-dbus', + 'org.freedesktop.Accounts.User.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'accounts-dbus', + 'org.freedesktop.Accounts.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'geoclue-manager-dbus', + 'org.freedesktop.GeoClue2.Manager.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'hostname1-dbus', + 'org.freedesktop.hostname1.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'portal-dbus', + 'org.freedesktop.impl.portal.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'login1-manager-dbus', + 'org.freedesktop.login1.Manager.xml', + 'org.freedesktop.login1', + false, + false, + ], + [ + 'login1-session-dbus', + 'org.freedesktop.login1.Session.xml', + 'org.freedesktop.login1', + false, + false, + ], + ['calls-dbus', 'org.gnome.Calls.Call.xml', 'org.gnome', false, true], + [ + 'calls-emergency-dbus', + 'org.gnome.Calls.EmergencyCalls.xml', + 'org.gnome.Calls', + false, + false, + ], + [ + 'gsd-color-dbus', + 'org.gnome.SettingsDaemon.Color.xml', + 'org.gnome.SettingsDaemon', + false, + false, + ], + [ + 'gsd-rfkill-dbus', + 'org.gnome.SettingsDaemon.Rfkill.xml', + 'org.gnome.SettingsDaemon', + false, + false, + ], + [ + 'gnome-session-dbus', + 'org.gnome.SessionManager.xml', + 'org.gnome', + false, + false, + ], + [ + 'gnome-session-client-private-dbus', + 'org.gnome.SessionManager.ClientPrivate.xml', + 'org.gnome', + false, + false, + ], + [ + 'gnome-session-presence-dbus', + 'org.gnome.SessionManager.Presence.xml', + 'org.gnome', + false, + false, + ], + [ + 'gnome-shell-search-provider', + 'org.gnome.Shell.SearchProvider2.xml', + 'org.gnome.Shell', + false, + false, + ], + ['mpris-dbus', 'org.mpris.MediaPlayer2.xml', 'org.mpris', false, false], + ['phosh-wwan-ofono-dbus', 'org.ofono.xml', 'org', false, false], + ['phosh-osk0-dbus', 'sm.puri.OSK0.xml', 'sm.puri', false, false], +] + +# +# Protocols where Phosh is the DBus server: +# +dbus_server_protos = [ + # Sorted by xml filename: + [ + 'phosh-dbus-debug-control', + 'mobi.phosh.Shell.DebugControl.xml', + 'mobi.phosh.Shell', + false, + false, + ], + [ + 'geoclue-agent-dbus', + 'org.freedesktop.GeoClue2.Agent.xml', + 'org.freedesktop', + false, + false, + ], + [ + 'notify-dbus', + 'org.freedesktop.Notifications.xml', + 'org.freedesktop', + false, + false, + ], + ['phosh-gnome-shell-dbus', 'org.gnome.Shell.xml', 'org', false, false], + [ + 'phosh-display-dbus', + 'org.gnome.Mutter.DisplayConfig.xml', + 'org.gnome.Mutter', + false, + false, + ], + [ + 'phosh-screen-saver-dbus', + 'org.gnome.ScreenSaver.xml', + 'org.gnome', + false, + false, + ], + [ + 'phosh-brightness-dbus', + 'org.gnome.Shell.Brightness.xml', + 'org.gnome.Shell', + false, + false, + ], + [ + 'phosh-screenshot-dbus', + 'org.gnome.Shell.Screenshot.xml', + 'org.gnome.Shell', + true, + false, + ], + [ + 'phosh-end-session-dialog-dbus', + 'org.gnome.SessionManager.EndSessionDialog.xml', + 'org.gnome.SessionManager', + false, + false, + ], + [ + 'phosh-gtk-mountoperation-dbus', + 'org.Gtk.MountOperationHandler.xml', + 'org.Gtk', + false, + false, + ], + [ + 'phosh-searchd', + 'mobi.phosh.Shell.Search.xml', + 'mobi.phosh.Shell', + false, + false, + ], +] + +foreach p : dbus_client_protos + dbus_server_protos + name = p[0] + xml = p[1] + prefix = p[2] + is_libphosh_header = p[3] + object_manager = p[4] + targets = gnome.gdbus_codegen( + name, + xml, + object_manager: object_manager, + interface_prefix: prefix, + install_header: bindings_lib and is_libphosh_header, + install_dir: dbus_inc_dir, + namespace: dbus_prefix, + ) + generated_dbus_sources += targets[0] + generated_dbus_headers += targets[1] + if is_libphosh_header + libphosh_generated_dbus_sources += targets[0] + libphosh_generated_dbus_headers += targets[1] + endif +endforeach + +# TODO: Use `dbus_prefix` as namespace +targets = gnome.gdbus_codegen( + 'phosh-idle-dbus', + 'org.gnome.Mutter.IdleMonitor.xml', + interface_prefix: 'org.gnome.Mutter', + object_manager: true, + namespace: 'PhoshIdleDBus', +) +generated_dbus_sources += targets[0] +generated_dbus_headers += targets[1] diff --git a/src/dbus/mobi.phosh.Shell.DebugControl.xml b/src/dbus/mobi.phosh.Shell.DebugControl.xml new file mode 100644 index 000000000..dfc033eb3 --- /dev/null +++ b/src/dbus/mobi.phosh.Shell.DebugControl.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + diff --git a/src/dbus/mobi.phosh.Shell.Search.xml b/src/dbus/mobi.phosh.Shell.Search.xml new file mode 100644 index 000000000..733d74600 --- /dev/null +++ b/src/dbus/mobi.phosh.Shell.Search.xml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/net.hadess.SensorProxy.xml b/src/dbus/net.hadess.SensorProxy.xml new file mode 100644 index 000000000..0a305f4d6 --- /dev/null +++ b/src/dbus/net.hadess.SensorProxy.xml @@ -0,0 +1,215 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.Gtk.MountOperationHandler.xml b/src/dbus/org.Gtk.MountOperationHandler.xml new file mode 100644 index 000000000..8b03a2eea --- /dev/null +++ b/src/dbus/org.Gtk.MountOperationHandler.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.Accounts.User.xml b/src/dbus/org.freedesktop.Accounts.User.xml new file mode 100644 index 000000000..81129b842 --- /dev/null +++ b/src/dbus/org.freedesktop.Accounts.User.xml @@ -0,0 +1,38 @@ + + + + + + + + + The username of the user. + + + + + + + + + + The userʼs real name. + + + + + + + + + + The filename of a png file containing the userʼs icon. + + + + + + + diff --git a/src/dbus/org.freedesktop.Accounts.xml b/src/dbus/org.freedesktop.Accounts.xml new file mode 100644 index 000000000..5ee7ff77f --- /dev/null +++ b/src/dbus/org.freedesktop.Accounts.xml @@ -0,0 +1,29 @@ + + + + + + + + + The username to look up + + + Object path of user + + + + + + Finds a user by its username. + + + + if no user with the given username exists + + + + + diff --git a/src/dbus/org.freedesktop.GeoClue2.Agent.xml b/src/dbus/org.freedesktop.GeoClue2.Agent.xml new file mode 100644 index 000000000..27736016c --- /dev/null +++ b/src/dbus/org.freedesktop.GeoClue2.Agent.xml @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.GeoClue2.Manager.xml b/src/dbus/org.freedesktop.GeoClue2.Manager.xml new file mode 100644 index 000000000..28db3c637 --- /dev/null +++ b/src/dbus/org.freedesktop.GeoClue2.Manager.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/src/dbus/org.freedesktop.Notifications.xml b/src/dbus/org.freedesktop.Notifications.xml new file mode 100644 index 000000000..7d35dad2c --- /dev/null +++ b/src/dbus/org.freedesktop.Notifications.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.hostname1.xml b/src/dbus/org.freedesktop.hostname1.xml new file mode 100644 index 000000000..d8e469b02 --- /dev/null +++ b/src/dbus/org.freedesktop.hostname1.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/src/dbus/org.freedesktop.impl.portal.xml b/src/dbus/org.freedesktop.impl.portal.xml new file mode 100644 index 000000000..7d8ff8fa0 --- /dev/null +++ b/src/dbus/org.freedesktop.impl.portal.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/dbus/org.freedesktop.login1.Manager.xml b/src/dbus/org.freedesktop.login1.Manager.xml new file mode 100644 index 000000000..613296a08 --- /dev/null +++ b/src/dbus/org.freedesktop.login1.Manager.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.freedesktop.login1.Session.xml b/src/dbus/org.freedesktop.login1.Session.xml new file mode 100644 index 000000000..1d20570e2 --- /dev/null +++ b/src/dbus/org.freedesktop.login1.Session.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Calls.Call.xml b/src/dbus/org.gnome.Calls.Call.xml new file mode 100644 index 000000000..5c7b8bd44 --- /dev/null +++ b/src/dbus/org.gnome.Calls.Call.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + A one character string. One of: 0-9,A-D,* or #. + + + + + + + + + The Id identifying the call, e.g. a phone number + + + + + + + The DisplayName of the calling party, e.g. from address book + + + + + + + The path to an image to display for this call. + + + + + + + The protocol used for this call + + + + + + + Whether the call is encrypted. This does not indicate anything about the + type of encryption being used. + + + + + + + Whether the call can do DTMF + + + + + diff --git a/src/dbus/org.gnome.Calls.EmergencyCalls.xml b/src/dbus/org.gnome.Calls.EmergencyCalls.xml new file mode 100644 index 000000000..ef37ee0f0 --- /dev/null +++ b/src/dbus/org.gnome.Calls.EmergencyCalls.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Mutter.DisplayConfig.xml b/src/dbus/org.gnome.Mutter.DisplayConfig.xml new file mode 100644 index 000000000..fe0d10606 --- /dev/null +++ b/src/dbus/org.gnome.Mutter.DisplayConfig.xml @@ -0,0 +1,509 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Mutter.IdleMonitor.xml b/src/dbus/org.gnome.Mutter.IdleMonitor.xml new file mode 100644 index 000000000..34a26dd93 --- /dev/null +++ b/src/dbus/org.gnome.Mutter.IdleMonitor.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.ScreenSaver.xml b/src/dbus/org.gnome.ScreenSaver.xml new file mode 100644 index 000000000..ec9f4aea9 --- /dev/null +++ b/src/dbus/org.gnome.ScreenSaver.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.SessionManager.ClientPrivate.xml b/src/dbus/org.gnome.SessionManager.ClientPrivate.xml new file mode 100644 index 000000000..3c5dd6efb --- /dev/null +++ b/src/dbus/org.gnome.SessionManager.ClientPrivate.xml @@ -0,0 +1,123 @@ + + + + + + + + Whether or not it is OK to proceed + + + + + The reason string + + + + + This method should be used by the client in response to + the QueryEndSession + and EndSession signals. + + + + + + + + Stop client + + + The client should stop and remove itself from the session in + response to this signal. + + + + + + + + + Flags + + + + + This signal is used to inform the client that the + session is about to end. The client must respond by + calling + EndSessionResponse + within one second of the signal emission. + + + The flags may include: + + + 1 + Logout is forced. + EndSessionResponse + reason and any inhibit from client will be + ignored. + + + + + If the client responds with an EndSessionResponse is-ok + argument equal to FALSE and a reason then this reason may + be displayed to the user. + + + The client must not attempt to perform any actions or + interact with the user in response to this signal. Any + actions required for a clean shutdown should take place in + response to the + EndSession signal. + + + The client should limit operations until either a + EndSession + CancelEndSession + signal is received. + + + + + + + + + Flags + + + + + This signal is used to inform the client that the + session is about to end. The client must respond by + calling + EndSessionResponse + within ten seconds of the signal emission. + + + The client must not attempt to interact with the user in + response to this signal. The application will be given a + maximum of ten seconds to perform any actions required for + a clean shutdown. + + + + + + + + + + This signal indicates to the client that a previous emission of + QueryEndSession + has been cancelled. The client should resume normal operations. + + + + + + + diff --git a/src/dbus/org.gnome.SessionManager.EndSessionDialog.xml b/src/dbus/org.gnome.SessionManager.EndSessionDialog.xml new file mode 100644 index 000000000..38f27572a --- /dev/null +++ b/src/dbus/org.gnome.SessionManager.EndSessionDialog.xml @@ -0,0 +1,57 @@ + + + + + + + + + The type of dialog to show. + 0 for logout, 1 for shutdown, 2 for restart, 3 for hibernate, + 4 for suspend and 5 hybrid sleep. + + + + + + + Timestamp of the user-initiated event which triggered + the call, or 0 if the call was not triggered by an event. + + + + + + + The number of seconds which the dialog should stay open + before automatic action is taken. + + + + + + + The object paths of all inhibitors that are registered + for the action. + + + + + + This function opens a dialog which asks the user for confirmation + of a logout, poweroff or reboot action. The dialog has a timeout + after which the action is automatically taken, and it should show + the inhibitors to the user. + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.SessionManager.Presence.xml b/src/dbus/org.gnome.SessionManager.Presence.xml new file mode 100644 index 000000000..c3da67682 --- /dev/null +++ b/src/dbus/org.gnome.SessionManager.Presence.xml @@ -0,0 +1,49 @@ + + + + + + + + + + The status of the session. + + + The status parameter must be one of the following: + + + 0 + Available + + + 1 + Invisible + + + 2 + Busy + + + 3 + Idle + + + + + + + + + + The new status value + + + + + Indicates that the session status value has changed. + + + + + diff --git a/src/dbus/org.gnome.SessionManager.xml b/src/dbus/org.gnome.SessionManager.xml new file mode 100644 index 000000000..1cf9591b6 --- /dev/null +++ b/src/dbus/org.gnome.SessionManager.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.SettingsDaemon.Color.xml b/src/dbus/org.gnome.SettingsDaemon.Color.xml new file mode 100644 index 000000000..c41339524 --- /dev/null +++ b/src/dbus/org.gnome.SettingsDaemon.Color.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/src/dbus/org.gnome.SettingsDaemon.Rfkill.xml b/src/dbus/org.gnome.SettingsDaemon.Rfkill.xml new file mode 100644 index 000000000..628b17ff9 --- /dev/null +++ b/src/dbus/org.gnome.SettingsDaemon.Rfkill.xml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/dbus/org.gnome.Shell.Brightness.xml b/src/dbus/org.gnome.Shell.Brightness.xml new file mode 100644 index 000000000..a0e639728 --- /dev/null +++ b/src/dbus/org.gnome.Shell.Brightness.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Shell.Screenshot.xml b/src/dbus/org.gnome.Shell.Screenshot.xml new file mode 100644 index 000000000..528b01ecb --- /dev/null +++ b/src/dbus/org.gnome.Shell.Screenshot.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Shell.SearchProvider2.xml b/src/dbus/org.gnome.Shell.SearchProvider2.xml new file mode 100644 index 000000000..9502340e4 --- /dev/null +++ b/src/dbus/org.gnome.Shell.SearchProvider2.xml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.gnome.Shell.xml b/src/dbus/org.gnome.Shell.xml new file mode 100644 index 000000000..347dad309 --- /dev/null +++ b/src/dbus/org.gnome.Shell.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/org.mpris.MediaPlayer2.xml b/src/dbus/org.mpris.MediaPlayer2.xml new file mode 100644 index 000000000..e1a32fffe --- /dev/null +++ b/src/dbus/org.mpris.MediaPlayer2.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + The number of microseconds to seek forward. + + + + + + + + + + + + diff --git a/src/dbus/org.ofono.xml b/src/dbus/org.ofono.xml new file mode 100644 index 000000000..2ed24b994 --- /dev/null +++ b/src/dbus/org.ofono.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/dbus/sm.puri.OSK0.xml b/src/dbus/sm.puri.OSK0.xml new file mode 100644 index 000000000..cbcd0fbce --- /dev/null +++ b/src/dbus/sm.puri.OSK0.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/src/debug-control.c b/src/debug-control.c new file mode 100644 index 000000000..5a8257b91 --- /dev/null +++ b/src/debug-control.c @@ -0,0 +1,186 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-debug-control" + +#include "phosh-config.h" + +#include "debug-control.h" +#include "phosh-enums.h" +#include "shell-priv.h" + +#include + +#define DEBUG_CONTROL_DBUS_PATH "/mobi/phosh/Shell/DebugControl" +#define DEBUG_CONTROL_DBUS_NAME "mobi.phosh.Shell.DebugControl" + +/** + * PhoshDebugControl: + * + * DBus Debug control interface + */ + +enum { + PROP_0, + PROP_EXPORTED, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshDebugControl { + PhoshDBusDebugControlSkeleton parent; + + guint dbus_name_id; + gboolean exported; +}; + +static void phosh_dbus_debug_control_iface_init (PhoshDBusDebugControlIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshDebugControl, phosh_debug_control, + PHOSH_DBUS_TYPE_DEBUG_CONTROL_SKELETON, + G_IMPLEMENT_INTERFACE (PHOSH_DBUS_TYPE_DEBUG_CONTROL, + phosh_dbus_debug_control_iface_init)) + +static void +phosh_dbus_debug_control_iface_init (PhoshDBusDebugControlIface *iface) +{ +} + + +static void +on_bus_acquired (GDBusConnection *connection, const char *name, gpointer user_data) +{ + PhoshDebugControl *self = user_data; + g_autoptr (GError) err = NULL; + + if (g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + DEBUG_CONTROL_DBUS_PATH, + &err)) { + self->exported = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPORTED]); + g_debug ("Debug interface exported on '%s'", DEBUG_CONTROL_DBUS_NAME); + } else { + g_warning ("Failed to export on %s: %s", DEBUG_CONTROL_DBUS_NAME, err->message); + } +} + + +static void +phosh_debug_control_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshDebugControl *self = PHOSH_DEBUG_CONTROL (object); + + switch (property_id) { + case PROP_EXPORTED: + phosh_debug_control_set_exported (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_debug_control_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshDebugControl *self = PHOSH_DEBUG_CONTROL (object); + + switch (property_id) { + case PROP_EXPORTED: + g_value_set_boolean (value, self->exported); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_debug_control_dispose (GObject *object) +{ + PhoshDebugControl *self = PHOSH_DEBUG_CONTROL (object); + + if (self->exported) { + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + self->exported = FALSE; + } + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + G_OBJECT_CLASS (phosh_debug_control_parent_class)->dispose (object); +} + + +static void +phosh_debug_control_class_init (PhoshDebugControlClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_debug_control_get_property; + object_class->set_property = phosh_debug_control_set_property; + object_class->dispose = phosh_debug_control_dispose; + + props[PROP_EXPORTED] = + g_param_spec_boolean ("exported", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_debug_control_init (PhoshDebugControl *self) +{ + g_object_bind_property (phosh_shell_get_default (), + "log-domains", + self, + "log-domains", + G_BINDING_SYNC_CREATE | G_BINDING_BIDIRECTIONAL); +} + + +PhoshDebugControl * +phosh_debug_control_new (void) +{ + return g_object_new (PHOSH_TYPE_DEBUG_CONTROL, NULL); +} + + +void +phosh_debug_control_set_exported (PhoshDebugControl *self, gboolean exported) +{ + g_assert (PHOSH_IS_DEBUG_CONTROL (self)); + + if (self->exported == exported) + return; + + if (exported) { + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + DEBUG_CONTROL_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + NULL, + NULL, + self, + NULL); + } else { + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + self->exported = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPORTED]); + } +} diff --git a/src/debug-control.h b/src/debug-control.h new file mode 100644 index 000000000..4eb0948d1 --- /dev/null +++ b/src/debug-control.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "phosh-dbus-debug-control.h" + +#include + +G_BEGIN_DECLS + + + +#define PHOSH_TYPE_DEBUG_CONTROL (phosh_debug_control_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshDebugControl, phosh_debug_control, PHOSH, DEBUG_CONTROL, + PhoshDBusDebugControlSkeleton) + +PhoshDebugControl *phosh_debug_control_new (void); +void phosh_debug_control_set_exported (PhoshDebugControl *self, + gboolean exported); + +G_END_DECLS diff --git a/src/default-media-player.c b/src/default-media-player.c new file mode 100644 index 000000000..443a65fde --- /dev/null +++ b/src/default-media-player.c @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-default-media-player" + +#include "phosh-config.h" + +#include "default-media-player.h" +#include "mpris-manager.h" +#include "shell-priv.h" + +/** + * PhoshDefaultMediaPlayer: + * + * The media player widget tracking the default player + * + * The default player is the last recently one added (so likely the one the user + * cares the most about). + */ + +typedef struct _PhoshDefaultMediaPlayer { + PhoshMediaPlayer parent_instance; + + PhoshMprisManager *manager; +} PhoshDefaultMediaPlayer; + +G_DEFINE_FINAL_TYPE (PhoshDefaultMediaPlayer, phosh_default_media_player, PHOSH_TYPE_MEDIA_PLAYER); + + +static void +phosh_default_media_player_dispose (GObject *object) +{ + PhoshDefaultMediaPlayer *self = PHOSH_DEFAULT_MEDIA_PLAYER (object); + + g_clear_object (&self->manager); + + G_OBJECT_CLASS (phosh_default_media_player_parent_class)->dispose (object); +} + + +static void +phosh_default_media_player_class_init (PhoshDefaultMediaPlayerClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + + object_class->dispose = phosh_default_media_player_dispose; +} + + +static void +on_player_changed (PhoshDefaultMediaPlayer *self, GParamSpec *pspec, PhoshMprisManager *manager) +{ + g_assert (PHOSH_IS_MEDIA_PLAYER (self)); + g_assert (PHOSH_IS_MPRIS_MANAGER (self->manager)); + + phosh_media_player_set_player (PHOSH_MEDIA_PLAYER (self), + phosh_mpris_manager_get_player (self->manager)); +} + + +static void +phosh_default_media_player_init (PhoshDefaultMediaPlayer *self) +{ + PhoshMprisManager *manager = phosh_shell_get_mpris_manager (phosh_shell_get_default ()); + + if (manager) { + self->manager = g_object_ref (manager); + + g_signal_connect_object (self->manager, + "notify::player", + G_CALLBACK (on_player_changed), + self, + G_CONNECT_SWAPPED); + on_player_changed (self, NULL, self->manager); + } +} + + +GtkWidget * +phosh_default_media_player_new (void) +{ + return g_object_new (PHOSH_TYPE_MEDIA_PLAYER, NULL); +} diff --git a/src/default-media-player.h b/src/default-media-player.h new file mode 100644 index 000000000..7247851a6 --- /dev/null +++ b/src/default-media-player.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "media-player.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DEFAULT_MEDIA_PLAYER (phosh_default_media_player_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshDefaultMediaPlayer, phosh_default_media_player, PHOSH, + DEFAULT_MEDIA_PLAYER, PhoshMediaPlayer) + +GtkWidget *phosh_default_media_player_new (void); + +G_END_DECLS diff --git a/src/docked-info.c b/src/docked-info.c new file mode 100644 index 000000000..f2c08aa6c --- /dev/null +++ b/src/docked-info.c @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-docked-info" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "docked-info.h" +#include "docked-manager.h" + +/** + * PhoshDockedInfo: + * + * A widget to display the docked status + * + * #PhoshDockedInfo displays whether the device is docked + */ + +enum { + PROP_0, + PROP_ENABLED, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshDockedInfo { + PhoshStatusIcon parent; + + gboolean enabled; + gboolean present; + PhoshDockedManager *manager; +}; +G_DEFINE_TYPE (PhoshDockedInfo, phosh_docked_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_docked_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshDockedInfo *self = PHOSH_DOCKED_INFO (object); + + switch (property_id) { + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_docked_mode_enabled (PhoshDockedInfo *self, GParamSpec *pspec, PhoshDockedManager *manager) +{ + gboolean enabled; + + g_debug ("Updating docked status"); + g_return_if_fail (PHOSH_IS_DOCKED_INFO (self)); + g_return_if_fail (PHOSH_IS_DOCKED_MANAGER (manager)); + + enabled = phosh_docked_manager_get_enabled (manager); + if (self->enabled == enabled) + return; + + self->enabled = enabled; + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), + enabled ? _("Docked") : _("Undocked")); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + + +static void +on_docked_present (PhoshDockedInfo *self, GParamSpec *pspec, PhoshDockedManager *manager) +{ + gboolean can_dock; + + g_return_if_fail (PHOSH_IS_DOCKED_INFO (self)); + g_return_if_fail (PHOSH_IS_DOCKED_MANAGER (manager)); + + can_dock = phosh_docked_manager_get_can_dock (manager); + if (self->present == can_dock) + return; + + self->present = can_dock; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_docked_info_idle_init (PhoshStatusIcon *icon) +{ + PhoshDockedInfo *self = PHOSH_DOCKED_INFO (icon); + + g_object_bind_property (self->manager, "icon-name", self, "icon-name", + G_BINDING_SYNC_CREATE); + + /* We don't use a binding for self->enabled so we can keep + the property r/o */ + g_signal_connect_swapped (self->manager, + "notify::can-dock", + G_CALLBACK (on_docked_present), + self); + on_docked_present (self, NULL, self->manager); + + g_signal_connect_swapped (self->manager, + "notify::enabled", + G_CALLBACK (on_docked_mode_enabled), + self); + on_docked_mode_enabled (self, NULL, self->manager); +} + + +static void +phosh_docked_info_constructed (GObject *object) +{ + PhoshDockedInfo *self = PHOSH_DOCKED_INFO (object); + PhoshShell *shell; + + G_OBJECT_CLASS (phosh_docked_info_parent_class)->constructed (object); + + shell = phosh_shell_get_default (); + self->manager = g_object_ref (phosh_shell_get_docked_manager (shell)); + + if (self->manager == NULL) { + g_warning ("Failed to get docked manager"); + return; + } +} + + +static void +phosh_docked_info_dispose (GObject *object) +{ + PhoshDockedInfo *self = PHOSH_DOCKED_INFO (object); + + if (self->manager) { + g_signal_handlers_disconnect_by_data (self->manager, self); + g_clear_object (&self->manager); + } + + G_OBJECT_CLASS (phosh_docked_info_parent_class)->dispose (object); +} + + +static void +phosh_docked_info_class_init (PhoshDockedInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshStatusIconClass *status_icon_class = PHOSH_STATUS_ICON_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_docked_info_constructed; + object_class->dispose = phosh_docked_info_dispose; + object_class->get_property = phosh_docked_info_get_property; + + status_icon_class->idle_init = phosh_docked_info_idle_init; + + gtk_widget_class_set_css_name (widget_class, "phosh-docked-info"); + + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + "enabled", + "Whether the docked is enabled", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + props[PROP_PRESENT] = + g_param_spec_boolean ("present", + "Present", + "Whether docked hardware is present", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_docked_info_init (PhoshDockedInfo *self) +{ + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Undocked")); +} + + +GtkWidget * +phosh_docked_info_new (void) +{ + return g_object_new (PHOSH_TYPE_DOCKED_INFO, NULL); +} diff --git a/src/docked-info.h b/src/docked-info.h new file mode 100644 index 000000000..bdcbfd0d6 --- /dev/null +++ b/src/docked-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DOCKED_INFO (phosh_docked_info_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshDockedInfo, phosh_docked_info, PHOSH, DOCKED_INFO, PhoshStatusIcon) + +GtkWidget * phosh_docked_info_new (void); + +G_END_DECLS diff --git a/src/docked-manager.c b/src/docked-manager.c new file mode 100644 index 000000000..35d97dfc1 --- /dev/null +++ b/src/docked-manager.c @@ -0,0 +1,329 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-docked-manager" + +#include "phosh-config.h" + +#include "docked-manager.h" +#include "mode-manager.h" +#include "shell-priv.h" + +#define DOCKED_DISABLED_ICON "phone-undocked-symbolic" +#define DOCKED_ENABLED_ICON "phone-docked-symbolic" + +#define PHOC_KEY_MAXIMIZE "auto-maximize" +#define A11Y_KEY_OSK "screen-keyboard-enabled" +#define WM_KEY_LAYOUT "button-layout" +#define GTK_KEY_IS_PHONE "is-phone" + +/** + * PhoshDockedManager: + * + * Handles 'docking" the phone to additional hardware + * + * #PhoshDockedManager allows to dock the phone to additional hardware + * and performs the necessary configuration changes + * (disable OSK, don't maximize windows by default, ...) + */ + +enum { + PROP_0, + PROP_MODE_MANAGER, + PROP_ICON_NAME, + PROP_ENABLED, + PROP_CAN_DOCK, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshDockedManager { + GObject parent; + + gboolean enabled; + gboolean can_dock; + const char *icon_name; + + PhoshModeManager *mode_manager; + + GSettings *phoc_settings; + GSettings *wm_settings; + GSettings *a11y_settings; + GSettings *gtk_settings; + GSettings *gtk4_settings; +}; +G_DEFINE_TYPE (PhoshDockedManager, phosh_docked_manager, G_TYPE_OBJECT); + + +static void +phosh_docked_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshDockedManager *self = PHOSH_DOCKED_MANAGER (object); + + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_CAN_DOCK: + g_value_set_boolean (value, self->can_dock); + break; + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_docked_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshDockedManager *self = PHOSH_DOCKED_MANAGER (object); + + switch (property_id) { + case PROP_MODE_MANAGER: + /* construct only */ + self->mode_manager = g_value_dup_object (value); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODE_MANAGER]); + break; + case PROP_ENABLED: + phosh_docked_manager_set_enabled (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +mode_changed_cb (PhoshDockedManager *self, GParamSpec *pspec, PhoshModeManager *manager) +{ + gboolean can_dock = FALSE; + + g_return_if_fail (PHOSH_IS_DOCKED_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MODE_MANAGER (manager)); + g_return_if_fail (self->mode_manager == manager); + + /* + * Desktops, laptops and phones with enough external hardware should get floating + * windows, etc. Unknown hardware is assumed to be a phone. + */ + if (phosh_mode_manager_get_mimicry (manager) != PHOSH_MODE_DEVICE_TYPE_PHONE && + phosh_mode_manager_get_mimicry (manager) != PHOSH_MODE_DEVICE_TYPE_TABLET && + phosh_mode_manager_get_mimicry (manager) != PHOSH_MODE_DEVICE_TYPE_UNKNOWN) + can_dock = TRUE; + + if (can_dock == self->can_dock) + return; + + g_debug ("Docked mode possible: %d", can_dock); + self->can_dock = can_dock; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_DOCK]); + + /* Automatically enable/disable docked mode */ + phosh_docked_manager_set_enabled (self, can_dock); +} + + +static void +phosh_docked_manager_constructed (GObject *object) +{ + PhoshDockedManager *self = PHOSH_DOCKED_MANAGER (object); + GSettingsSchemaSource *schema_source = g_settings_schema_source_get_default(); + g_autoptr (GSettingsSchema) schema = NULL; + g_autoptr (GSettingsSchema) gtk4_schema = NULL; + + + G_OBJECT_CLASS (phosh_docked_manager_parent_class)->constructed (object); + + self->phoc_settings = g_settings_new ("sm.puri.phoc"); + self->a11y_settings = g_settings_new ("org.gnome.desktop.a11y.applications"); + self->wm_settings = g_settings_new ("org.gnome.desktop.wm.preferences"); + + /* This is a downstream only schema that is supposed to go away once GTK + * GTK is adaptive (https://source.puri.sm/Librem5/gtk/-/merge_requests/18) */ + schema = g_settings_schema_source_lookup (schema_source, "org.gtk.Settings.Purism", TRUE); + if (schema && g_settings_schema_has_key (schema, GTK_KEY_IS_PHONE)) + self->gtk_settings = g_settings_new ("org.gtk.Settings.Purism"); + + gtk4_schema = g_settings_schema_source_lookup (schema_source, "org.gtk.gtk4.Settings.Purism", TRUE); + if (gtk4_schema && g_settings_schema_has_key (gtk4_schema, GTK_KEY_IS_PHONE)) + self->gtk4_settings = g_settings_new ("org.gtk.gtk4.Settings.Purism"); + + g_object_connect ( + self->mode_manager, + "swapped-object-signal::notify::device-type", G_CALLBACK (mode_changed_cb), self, + "swapped-object-signal::notify::mimicry", G_CALLBACK (mode_changed_cb), self, + NULL); + mode_changed_cb (self, NULL, self->mode_manager); +} + + +static void +phosh_docked_manager_dispose (GObject *object) +{ + PhoshDockedManager *self = PHOSH_DOCKED_MANAGER (object); + + g_clear_object (&self->phoc_settings); + g_clear_object (&self->a11y_settings); + g_clear_object (&self->wm_settings); + g_clear_object (&self->gtk_settings); + g_clear_object (&self->gtk4_settings); + g_clear_object (&self->mode_manager); + + G_OBJECT_CLASS (phosh_docked_manager_parent_class)->dispose (object); +} + + +static void +phosh_docked_manager_class_init (PhoshDockedManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_docked_manager_constructed; + object_class->dispose = phosh_docked_manager_dispose; + object_class->get_property = phosh_docked_manager_get_property; + object_class->set_property = phosh_docked_manager_set_property; + + props[PROP_MODE_MANAGER] = + g_param_spec_object ("mode-manager", + "Mode manager", + "The hw mode object", + PHOSH_TYPE_MODE_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "icon name", + "The docked icon name", + DOCKED_DISABLED_ICON, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + "enabled", + "Whether docked mode is enabled", + FALSE, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_CAN_DOCK] = + g_param_spec_boolean ("can-dock", + "Can dock", + "Whether the device can be docked", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_docked_manager_init (PhoshDockedManager *self) +{ + self->icon_name = DOCKED_DISABLED_ICON; + self->can_dock = -1; + self->enabled = -1; +} + + +PhoshDockedManager * +phosh_docked_manager_new (PhoshModeManager *mode_manager) +{ + return PHOSH_DOCKED_MANAGER (g_object_new (PHOSH_TYPE_DOCKED_MANAGER, + "mode-manager", mode_manager, + NULL)); +} + + +const char * +phosh_docked_manager_get_icon_name (PhoshDockedManager *self) +{ + g_return_val_if_fail (PHOSH_IS_DOCKED_MANAGER (self), NULL); + + return self->icon_name; +} + + +gboolean +phosh_docked_manager_get_enabled (PhoshDockedManager *self) +{ + g_return_val_if_fail (PHOSH_IS_DOCKED_MANAGER (self), FALSE); + + return self->enabled; +} + + +gboolean +phosh_docked_manager_get_can_dock (PhoshDockedManager *self) +{ + g_return_val_if_fail (PHOSH_IS_DOCKED_MANAGER (self), FALSE); + + return self->can_dock; +} + + +void +phosh_docked_manager_set_enabled (PhoshDockedManager *self, gboolean enable) +{ + const char *icon_name; + + g_return_if_fail (PHOSH_IS_DOCKED_MANAGER (self)); + g_return_if_fail ((enable && self->can_dock) || !enable); + + if (self->enabled == enable) + return; + + g_object_freeze_notify (G_OBJECT (self)); + + self->enabled = enable; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); + + if (enable) + g_settings_reset (self->wm_settings, WM_KEY_LAYOUT); + else + g_settings_set_string (self->wm_settings, WM_KEY_LAYOUT, "appmenu:"); + + /* we could bind one boolean property via g_settings_bind but that would spread + * mode setup over several places */ + g_settings_set_boolean (self->phoc_settings, PHOC_KEY_MAXIMIZE, !enable); + g_settings_set_boolean (self->a11y_settings, A11Y_KEY_OSK, !enable); + if (self->gtk_settings) + g_settings_set_boolean (self->gtk_settings, GTK_KEY_IS_PHONE, !enable); + if (self->gtk4_settings) + g_settings_set_boolean (self->gtk4_settings, GTK_KEY_IS_PHONE, !enable); + + /* TODO: Other icons for non phones? */ + icon_name = enable ? DOCKED_ENABLED_ICON : DOCKED_DISABLED_ICON; + if (icon_name != self->icon_name) { + self->icon_name = icon_name; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + } + + g_object_thaw_notify (G_OBJECT (self)); + + if (!enable) { + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitor *monitor = phosh_shell_get_builtin_monitor (shell); + if (monitor) + phosh_shell_set_primary_monitor (shell, monitor); + } + g_debug ("Docked mode %sabled", self->enabled ? "en" : "dis"); +} diff --git a/src/docked-manager.h b/src/docked-manager.h new file mode 100644 index 000000000..2e88298c2 --- /dev/null +++ b/src/docked-manager.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DOCKED_MANAGER (phosh_docked_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshDockedManager, phosh_docked_manager, PHOSH, DOCKED_MANAGER, GObject) + +PhoshDockedManager *phosh_docked_manager_new (PhoshModeManager *mode_manager); +const char *phosh_docked_manager_get_icon_name (PhoshDockedManager *self); +gboolean phosh_docked_manager_get_enabled (PhoshDockedManager *self); +gboolean phosh_docked_manager_get_can_dock (PhoshDockedManager *self); +void phosh_docked_manager_set_enabled (PhoshDockedManager *self, gboolean enabled); + +G_END_DECLS diff --git a/src/drag-surface.c b/src/drag-surface.c new file mode 100644 index 000000000..e803e5488 --- /dev/null +++ b/src/drag-surface.c @@ -0,0 +1,579 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-drag-surface" + +#include "phosh-config.h" + +#include "phosh-enums.h" +#include "drag-surface.h" +#include "layersurface-priv.h" + +/** + * PhoshDragSurface: + * + * A drgable layer surface + * + * A layer surface that can be dragged in ne direction via gestures. + * + * See #PhoshTopPanel for a usage example. Note that you need to + * update folded/unfolded margins on the #PhoshLayerSurface's + * `configured` event to adjust it to the proper sizes. + */ + +enum { + PROP_0, + PROP_LAYER_SHELL_EFFECTS, + PROP_MARGIN_FOLDED, + PROP_MARGIN_UNFOLDED, + PROP_THRESHOLD, + PROP_DRAG_MODE, + PROP_DRAG_HANDLE, + PROP_DRAG_STATE, + PROP_EXCLUSIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + SIGNAL_DRAGGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +typedef struct _PhoshDragSurfacePrivate { + struct zphoc_layer_shell_effects_v1 *layer_shell_effects; + struct zphoc_draggable_layer_surface_v1 *drag_surface; + + int margin_folded; + int margin_unfolded; + double threshold; + PhoshDragSurfaceState drag_state; + PhoshDragSurfaceDragMode drag_mode; + guint drag_handle; + guint exclusive; +} PhoshDragSurfacePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshDragSurface, phosh_drag_surface, PHOSH_TYPE_LAYER_SURFACE) + + +static void +phosh_drag_surface_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (object); + PhoshDragSurfacePrivate *priv = phosh_drag_surface_get_instance_private (self); + + switch (property_id) { + case PROP_LAYER_SHELL_EFFECTS: + priv->layer_shell_effects = g_value_get_pointer (value); + break; + case PROP_MARGIN_FOLDED: + phosh_drag_surface_set_margin (self, g_value_get_int (value), priv->margin_unfolded); + break; + case PROP_MARGIN_UNFOLDED: + phosh_drag_surface_set_margin (self, priv->margin_folded, g_value_get_int (value)); + break; + case PROP_THRESHOLD: + phosh_drag_surface_set_threshold (self, g_value_get_double (value)); + break; + case PROP_DRAG_MODE: + phosh_drag_surface_set_drag_mode (self, g_value_get_enum (value)); + break; + case PROP_DRAG_HANDLE: + phosh_drag_surface_set_drag_handle (self, g_value_get_uint (value)); + break; + case PROP_EXCLUSIVE: + phosh_drag_surface_set_exclusive (self, g_value_get_uint (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_drag_surface_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (object); + PhoshDragSurfacePrivate *priv = phosh_drag_surface_get_instance_private (self); + + switch (property_id) { + case PROP_LAYER_SHELL_EFFECTS: + g_value_set_pointer (value, priv->layer_shell_effects); + break; + case PROP_MARGIN_FOLDED: + g_value_set_int (value, priv->margin_folded); + break; + case PROP_MARGIN_UNFOLDED: + g_value_set_int (value, priv->margin_unfolded); + break; + case PROP_THRESHOLD: + g_value_set_double (value, priv->threshold); + break; + case PROP_DRAG_MODE: + g_value_set_enum (value, priv->drag_mode); + break; + case PROP_DRAG_HANDLE: + g_value_set_uint (value, priv->drag_handle); + break; + case PROP_DRAG_STATE: + g_value_set_enum (value, priv->drag_state); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +drag_surface_handle_drag_end (void *data, + struct zphoc_draggable_layer_surface_v1 *drag_surface_, + uint32_t state) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (data); + PhoshDragSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + + priv = phosh_drag_surface_get_instance_private (self); + + if (state == priv->drag_state) + return; + + priv->drag_state = state; + g_debug ("DragSurface %p: state, %d", self, priv->drag_state); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_STATE]); +} + + +static void +drag_surface_handle_dragged (void *data, + struct zphoc_draggable_layer_surface_v1 *drag_surface_, + int margin) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (data); + PhoshDragSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + + priv = phosh_drag_surface_get_instance_private (self); + + g_signal_emit (self, signals[SIGNAL_DRAGGED], 0, margin); + + if (priv->drag_state == PHOSH_DRAG_SURFACE_STATE_DRAGGED) + return; + + priv->drag_state = PHOSH_DRAG_SURFACE_STATE_DRAGGED; + g_debug ("DragSurface %p: state, %d", self, priv->drag_state); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_STATE]); +} + + +const struct zphoc_draggable_layer_surface_v1_listener drag_surface_listener = { + .drag_end = drag_surface_handle_drag_end, + .dragged = drag_surface_handle_dragged, +}; + + +static void +phosh_drag_surface_configured (PhoshLayerSurface *layer_surface) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (layer_surface); + PhoshLayerSurfaceClass *parent_class = PHOSH_LAYER_SURFACE_CLASS (phosh_drag_surface_parent_class); + PhoshDragSurfacePrivate *priv = phosh_drag_surface_get_instance_private (self); + struct zwlr_layer_surface_v1 *wl_layer_surface = phosh_layer_surface_get_layer_surface (layer_surface); + + if (parent_class->configured) + parent_class->configured (layer_surface); + + if (priv->drag_surface) + return; + + /* Configure drag surface if not done yet */ + priv->drag_surface = zphoc_layer_shell_effects_v1_get_draggable_layer_surface (priv->layer_shell_effects, + wl_layer_surface); + zphoc_draggable_layer_surface_v1_add_listener (priv->drag_surface, &drag_surface_listener, self); +} + + +static enum zphoc_draggable_layer_surface_v1_drag_mode +drag_mode_to_phoc_drag_mode (PhoshDragSurfaceDragMode mode) +{ + switch (mode) { + case PHOSH_DRAG_SURFACE_DRAG_MODE_FULL: + return ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_MODE_FULL; + break; + case PHOSH_DRAG_SURFACE_DRAG_MODE_HANDLE: + return ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_MODE_HANDLE; + break; + case PHOSH_DRAG_SURFACE_DRAG_MODE_NONE: + return ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_MODE_NONE; + break; + default: + g_return_val_if_reached (ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_MODE_FULL); + } +} + + +static void +on_phosh_drag_surface_mapped (PhoshDragSurface *self, gpointer unused) +{ + PhoshDragSurfacePrivate *priv = phosh_drag_surface_get_instance_private (self); + enum zphoc_draggable_layer_surface_v1_drag_mode drag_mode; + + g_return_if_fail (priv->drag_surface); + + /* Commit initial state when mapped, we don't update any margins as they depend + on surface size */ + zphoc_draggable_layer_surface_v1_set_threshold (priv->drag_surface, + wl_fixed_from_double (priv->threshold)); + drag_mode = drag_mode_to_phoc_drag_mode(priv->drag_mode); + zphoc_draggable_layer_surface_v1_set_drag_mode (priv->drag_surface, + drag_mode); + zphoc_draggable_layer_surface_v1_set_drag_handle (priv->drag_surface, + priv->drag_handle); + zphoc_draggable_layer_surface_v1_set_exclusive (priv->drag_surface, + priv->exclusive); + phosh_layer_surface_wl_surface_commit (PHOSH_LAYER_SURFACE (self)); +} + + +static void +phosh_drag_surface_constructed (GObject *object) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (object); + + G_OBJECT_CLASS (phosh_drag_surface_parent_class)->constructed (object); + + g_signal_connect (self, "map", + G_CALLBACK (on_phosh_drag_surface_mapped), + NULL); +} + +static void +phosh_drag_surface_dispose (GObject *object) +{ + PhoshDragSurface *self = PHOSH_DRAG_SURFACE (object); + PhoshDragSurfacePrivate *priv = phosh_drag_surface_get_instance_private (self); + + g_clear_pointer (&priv->drag_surface, zphoc_draggable_layer_surface_v1_destroy); + + G_OBJECT_CLASS (phosh_drag_surface_parent_class)->dispose (object); +} + + +static void +phosh_drag_surface_class_init (PhoshDragSurfaceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + + object_class->get_property = phosh_drag_surface_get_property; + object_class->set_property = phosh_drag_surface_set_property; + object_class->constructed = phosh_drag_surface_constructed; + object_class->dispose = phosh_drag_surface_dispose; + + layer_surface_class->configured = phosh_drag_surface_configured; + + props[PROP_LAYER_SHELL_EFFECTS] = g_param_spec_pointer ("layer-shell-effects", + "", + "", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + props[PROP_MARGIN_FOLDED] = g_param_spec_int ("margin-folded", + "", + "", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_MARGIN_UNFOLDED] = g_param_spec_int ("margin-unfolded", + "", + "", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_THRESHOLD] = g_param_spec_double ("threshold", + "", + "", + G_MINDOUBLE, + G_MAXDOUBLE, + 1.0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_DRAG_MODE] = g_param_spec_enum ("drag-mode", + "", + "", + PHOSH_TYPE_DRAG_SURFACE_DRAG_MODE, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_DRAG_HANDLE] = g_param_spec_uint ("drag-handle", + "", + "", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_DRAG_STATE] = g_param_spec_enum ("drag-state", + "", + "", + PHOSH_TYPE_DRAG_SURFACE_STATE, + PHOSH_DRAG_SURFACE_STATE_FOLDED, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_EXCLUSIVE] = g_param_spec_uint ("exclusive", + "", + "", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[SIGNAL_DRAGGED] = g_signal_new ("dragged", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (PhoshDragSurfaceClass, dragged), + NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_INT); +} + + +static void +phosh_drag_surface_init (PhoshDragSurface *self) +{ +} + + +void +phosh_drag_surface_set_margin (PhoshDragSurface *self, int margin_folded, int margin_unfolded) +{ + PhoshDragSurfacePrivate *priv; + gboolean changed = FALSE; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + + priv = phosh_drag_surface_get_instance_private (self); + + if (priv->margin_folded != margin_folded) { + priv->margin_folded = margin_folded; + changed = TRUE; + } + + if (priv->margin_unfolded != margin_unfolded) { + priv->margin_folded = margin_folded; + changed = TRUE; + } + + if (changed == FALSE) + return; + + if (priv->drag_surface) + zphoc_draggable_layer_surface_v1_set_margins (priv->drag_surface, + priv->margin_folded, + priv->margin_unfolded); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MARGIN_FOLDED]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MARGIN_UNFOLDED]); +} + + +float +phosh_drag_surface_get_threshold (PhoshDragSurface *self) +{ + PhoshDragSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_DRAG_SURFACE (self), 0); + priv = phosh_drag_surface_get_instance_private (self); + + return priv->threshold; +} + + +void +phosh_drag_surface_set_threshold (PhoshDragSurface *self, double threshold) +{ + PhoshDragSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + + priv = phosh_drag_surface_get_instance_private (self); + + if (G_APPROX_VALUE (priv->threshold, threshold, FLT_EPSILON)) + return; + + priv->threshold = threshold; + if (priv->drag_surface) + zphoc_draggable_layer_surface_v1_set_threshold (priv->drag_surface, + wl_fixed_from_double (priv->threshold)); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_THRESHOLD]); +} + + +PhoshDragSurfaceState +phosh_drag_surface_get_drag_state (PhoshDragSurface *self) +{ + PhoshDragSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_DRAG_SURFACE (self), 0); + priv = phosh_drag_surface_get_instance_private (self); + + return priv->drag_state; +} + + +void +phosh_drag_surface_set_drag_state (PhoshDragSurface *self, + PhoshDragSurfaceState state) +{ + PhoshDragSurfacePrivate *priv; + enum zphoc_draggable_layer_surface_v1_drag_end_state drag_state; + + g_return_if_fail (state >= PHOSH_DRAG_SURFACE_STATE_FOLDED && + state <= PHOSH_DRAG_SURFACE_STATE_UNFOLDED); + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + priv = phosh_drag_surface_get_instance_private (self); + + if (priv->drag_state == state) + return; + + switch (state) { + case PHOSH_DRAG_SURFACE_STATE_FOLDED: + drag_state = ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_END_STATE_FOLDED; + break; + case PHOSH_DRAG_SURFACE_STATE_UNFOLDED: + drag_state = ZPHOC_DRAGGABLE_LAYER_SURFACE_V1_DRAG_END_STATE_UNFOLDED; + break; + case PHOSH_DRAG_SURFACE_STATE_DRAGGED: + default: + g_return_if_reached (); + } + + if (priv->drag_surface) + zphoc_draggable_layer_surface_v1_set_state (priv->drag_surface, drag_state); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_STATE]); +} + + +void +phosh_drag_surface_set_exclusive (PhoshDragSurface *self, guint exclusive) + +{ + PhoshDragSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + priv = phosh_drag_surface_get_instance_private (self); + + if (priv->exclusive == exclusive) + return; + + priv->exclusive = exclusive; + if (priv->drag_surface) + zphoc_draggable_layer_surface_v1_set_exclusive (priv->drag_surface, exclusive); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXCLUSIVE]); +} + + +PhoshDragSurfaceDragMode +phosh_drag_surface_get_drag_mode (PhoshDragSurface *self) +{ + PhoshDragSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_DRAG_SURFACE (self), 0); + priv = phosh_drag_surface_get_instance_private (self); + + return priv->drag_mode; +} + + +void +phosh_drag_surface_set_drag_mode (PhoshDragSurface *self, PhoshDragSurfaceDragMode mode) + +{ + PhoshDragSurfacePrivate *priv; + enum zphoc_draggable_layer_surface_v1_drag_mode drag_mode; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + priv = phosh_drag_surface_get_instance_private (self); + + if (priv->drag_mode == mode) + return; + priv->drag_mode = mode; + + if (priv->drag_surface) { + drag_mode = drag_mode_to_phoc_drag_mode(priv->drag_mode); + zphoc_draggable_layer_surface_v1_set_drag_mode (priv->drag_surface, drag_mode); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_MODE]); +} + + +void +phosh_drag_surface_set_drag_handle (PhoshDragSurface *self, guint handle) + +{ + PhoshDragSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_DRAG_SURFACE (self)); + priv = phosh_drag_surface_get_instance_private (self); + + if (priv->drag_handle == handle) + return; + priv->drag_handle = handle; + + if (priv->drag_surface) + zphoc_draggable_layer_surface_v1_set_drag_handle (priv->drag_surface, handle); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_HANDLE]); +} + + +guint +phosh_drag_surface_get_drag_handle (PhoshDragSurface *self) +{ + PhoshDragSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_DRAG_SURFACE (self), 0); + priv = phosh_drag_surface_get_instance_private (self); + + return priv->drag_handle; +} diff --git a/src/drag-surface.h b/src/drag-surface.h new file mode 100644 index 000000000..48dc83668 --- /dev/null +++ b/src/drag-surface.h @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "layersurface.h" +#include "phoc-layer-shell-effects-unstable-v1-client-protocol.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DRAG_SURFACE (phosh_drag_surface_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshDragSurface, phosh_drag_surface, PHOSH, DRAG_SURFACE, PhoshLayerSurface) + +/** + * PhoshDragSurfaceState: + * @PHOSH_DRAG_SURFACE_STATE_FOLDED: Surface is folded. + * @PHOSH_DRAG_SURFACE_STATE_UNFOLDED: Surface is unfolded. + * @PHOSH_DRAG_SURFACE_STATE_DRAGGED: Surface is being dragged. + * + * The state of the drag surface. + */ +typedef enum _PhoshDragSurfaceState { + PHOSH_DRAG_SURFACE_STATE_FOLDED, + PHOSH_DRAG_SURFACE_STATE_UNFOLDED, + PHOSH_DRAG_SURFACE_STATE_DRAGGED, +} PhoshDragSurfaceState; + +/** + * PhoshDragSurfaceDragMode: + * @PHOSH_DRAG_SURFACE_DRAG_MODE_FULL: Full surface is draggable + * @PHOSH_DRAG_SURFACE_DRAG_MODE_HANDLE: Handle area is draggable. + * @PHOSH_DRAG_SURFACE_DRAG_MODE_NONE: Surface is not draggable. + * + * The drag mode of the drag surface. Specifies how and where + * the surface is draggable. + */ +typedef enum _PhoshDragSurfaceDragMode { + PHOSH_DRAG_SURFACE_DRAG_MODE_FULL, + PHOSH_DRAG_SURFACE_DRAG_MODE_HANDLE, + PHOSH_DRAG_SURFACE_DRAG_MODE_NONE, +} PhoshDragSurfaceDragMode; + +/** + * PhoshDragSurfaceClass: + * @parent_class: The parent class + * @dragged: invoked when a surface is being dragged + */ +struct _PhoshDragSurfaceClass { + PhoshLayerSurfaceClass parent_class; + + void (*dragged) (PhoshDragSurface *self, int margin); +}; + +void phosh_drag_surface_set_margin (PhoshDragSurface *self, + int margin_folded, + int margin_unfolded); +float phosh_drag_surface_get_threshold (PhoshDragSurface *self); +void phosh_drag_surface_set_threshold (PhoshDragSurface *self, + double threshold); +PhoshDragSurfaceState phosh_drag_surface_get_drag_state (PhoshDragSurface *self); +void phosh_drag_surface_set_drag_state (PhoshDragSurface *self, + PhoshDragSurfaceState state); +void phosh_drag_surface_set_exclusive (PhoshDragSurface *self, + guint exclusive); +PhoshDragSurfaceDragMode phosh_drag_surface_get_drag_mode (PhoshDragSurface *self); +void phosh_drag_surface_set_drag_mode (PhoshDragSurface *self, + PhoshDragSurfaceDragMode mode); +guint phosh_drag_surface_get_drag_handle (PhoshDragSurface *self); +void phosh_drag_surface_set_drag_handle (PhoshDragSurface *self, + guint handle); + +G_END_DECLS diff --git a/src/emergency-calls-manager.c b/src/emergency-calls-manager.c new file mode 100644 index 000000000..d518f3f5d --- /dev/null +++ b/src/emergency-calls-manager.c @@ -0,0 +1,453 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#define G_LOG_DOMAIN "phosh-emergency-calls-manager" + +#include "phosh-config.h" + +#include "calls-emergency-dbus.h" +#include "emergency-calls-manager.h" +#include "emergency-contact.h" +#include "emergency-menu.h" +#include "util.h" +#include "shell-priv.h" + +#define CALLS_BUS_NAME "org.gnome.Calls" +#define CALLS_OBJECT_PATH "/org/gnome/Calls" + +#define EMERGENCY_CALLS_SETTINGS "sm.puri.phosh.emergency-calls" + +/** + * PhoshEmergencyCallsManager: + * @dbus_proxy: The DBus proxy object used for interacting with the emergency calls API. + * + * Manages emergency calls and contacts. Contacts are kept in + * a GListStore containing the emergency contacts form the calls API. + * + * #PhoshEmergencyCallsManager provides a GListStore containing the + * emergency contacts fetched from the calls API at `org.gnome.Calls`. + * It is designed to be used with [type@EmergencyContactRow] and the + * `gtk_list_box_bind_model` function. + * + * #PhoshEmergencyCallsManager also provides the [method@EmergencyCallsManager.call] method + * to call an emergency contact. + * If you are calling an emergency contact it is advised + * that you use `phosh_emergency_calls_call` or `phosh_emergency_calls_row_call` instead of + * calling `phosh_emergency_calls_manager_call` directly. + */ + +enum { + PROP_0, + PROP_EMERGENCY_CONTACTS, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + +enum { + DIAL_ERROR, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +struct _PhoshEmergencyCallsManager { + PhoshManager parent; + + PhoshDBusEmergencyCalls *dbus_proxy; + GCancellable *cancel; + + GListStore *emergency_contacts; + + PhoshEmergencyMenu *dialog; + GSettings *settings; + gboolean enabled; +}; +G_DEFINE_TYPE (PhoshEmergencyCallsManager, phosh_emergency_calls_manager, PHOSH_TYPE_MANAGER); + + +static void +phosh_emergency_calls_manager_set_if_enabled (PhoshEmergencyCallsManager *self, gboolean enabled) +{ + GAction *action; + + /* If disabled via settings we never enable */ + if (g_settings_get_boolean (self->settings, "enabled") == FALSE) { + g_debug ("Emergency calls disabled in settings"); + enabled = FALSE; + } + + if (enabled == self->enabled) + return; + + self->enabled = enabled; + g_message ("%s emergency calls", enabled ? "Enabling" : "Disabling"); + action = g_action_map_lookup_action (G_ACTION_MAP (phosh_shell_get_default ()), + "emergency.toggle-menu"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), enabled); +} + + +static void +close_menu (PhoshEmergencyCallsManager *self) +{ + g_debug ("Closing emergency call menu"); + + g_clear_pointer ((PhoshSystemModalDialog**)&self->dialog, phosh_system_modal_dialog_close); +} + + +static void +on_emergency_menu_done (PhoshEmergencyCallsManager *self) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + + close_menu (self); +} + + +static void +on_emergency_menu_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (data); + + if (self->dialog) { + close_menu (self); + return; + } + + self->dialog = phosh_emergency_menu_new (); + g_signal_connect_swapped (self->dialog, "done", + G_CALLBACK (on_emergency_menu_done), self); + + gtk_widget_set_visible (GTK_WIDGET (self->dialog), TRUE); +} + + +/** + * on_update_finish: + * + * Called when the DBus API returns the emergency contacts. + * From the phosh_emergency_calls_manager_idle_init function. + */ +static void +on_update_finish (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (user_data); + PhoshDBusEmergencyCalls *proxy = PHOSH_DBUS_EMERGENCY_CALLS (source_object); + g_autoptr (GError) err = NULL; + g_autoptr (GVariant) contacts = NULL; + GVariantIter iter; + gboolean success; + + success = phosh_dbus_emergency_calls_call_get_emergency_contacts_finish (proxy, + &contacts, + res, + &err); + if (success == FALSE) { + if (phosh_async_error_warn (err, "Failed to get emergency contacts")) { + /* Return right away on cancel as the manager is already gone */ + return; + } + goto out; + } + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + + g_list_store_remove_all (self->emergency_contacts); +#define CONTACT_FORMAT "(&s&si@a{sv})" + g_variant_iter_init (&iter, contacts); + while (TRUE) { + g_autoptr (PhoshEmergencyContact) contact = NULL; + g_autoptr (GVariant) properties = NULL; + const char *id = NULL; + const char *name = NULL; + gint32 source; + + if (g_variant_iter_next (&iter, CONTACT_FORMAT, &id, &name, &source, &properties) == FALSE) + break; + + contact = phosh_emergency_contact_new (id, name, source, properties); + g_list_store_append (self->emergency_contacts, contact); + /* At least one emergency contact found */ + success = TRUE; + } +#undef CONTACT_FORMAT + + out: + phosh_emergency_calls_manager_set_if_enabled (self, success); +} + +/** + * on_call_emergency_contact_finish: + * + * Called when the DBus API has processed the request to call an + * emergency contact or emergency service. From the + * #phosh_emergency_calls_manager_call function. + */ +static void +on_call_emergency_contact_finish (GObject *object, + GAsyncResult *res, + gpointer data) +{ + PhoshDBusEmergencyCalls *proxy; + PhoshEmergencyCallsManager *self; + gboolean success; + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_EMERGENCY_CALLS (object)); + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (data)); + proxy = PHOSH_DBUS_EMERGENCY_CALLS (object); + self = PHOSH_EMERGENCY_CALLS_MANAGER (data); + + success = phosh_dbus_emergency_calls_call_call_emergency_contact_finish (proxy, res, &err); + if (!success) { + g_signal_emit (self, signals[DIAL_ERROR], 0, err); + + phosh_async_error_warn (err, "Failed to call emergency contact"); + return; + } else { + phosh_shell_set_locked (phosh_shell_get_default (), TRUE); + close_menu (self); + } +} + + +/** + * phosh_emergency_calls_manager_update: + * @self: The #PhoshEmergencyCallsManager + * + * This function sends a request to the emergency contact DBus API and + * updates the list when it gets a response from the API. + * + * NOTE: This function does NOT create a new list it removes the + * existing items from the list and adds the new ones from the DBus + * API. + */ +static void +phosh_emergency_calls_manager_update (PhoshEmergencyCallsManager *self) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_PROXY (self->dbus_proxy)); + + phosh_dbus_emergency_calls_call_get_emergency_contacts ( + self->dbus_proxy, + self->cancel, + on_update_finish, + self); +} + + +static void +emergency_calls_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (object); + + switch (property_id) { + case PROP_EMERGENCY_CONTACTS: + g_value_set_object (value, G_OBJECT (self->emergency_contacts)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +on_emergency_numbers_changed (PhoshEmergencyCallsManager *self) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + + phosh_emergency_calls_manager_update (self); +} + +static void +on_name_owner_changed (PhoshEmergencyCallsManager *self) +{ + g_autofree char *name_owner = NULL; + + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->dbus_proxy)); + if (name_owner) + phosh_emergency_calls_manager_update (self); + else + phosh_emergency_calls_manager_set_if_enabled (self, FALSE); +} + +/** + * on_proxy_new_finish: + * + * Called when the DBus proxy is ready + */ +static void +on_proxy_new_finish (GObject *source_object, GAsyncResult *res, gpointer user_data) + +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (user_data); + PhoshDBusEmergencyCalls *proxy; + g_autoptr (GError) err = NULL; + + proxy = phosh_dbus_emergency_calls_proxy_new_for_bus_finish (res, &err); + if (proxy == NULL) { + phosh_async_error_warn (err, "Failed to get connect to emergency contacts DBus proxy"); + return; + } + + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + self->dbus_proxy = proxy; + + g_signal_connect_swapped (self->dbus_proxy, + "notify::g-name-owner", + G_CALLBACK (on_name_owner_changed), + self); + g_signal_connect_swapped (self->dbus_proxy, + "emergency-numbers-changed", + G_CALLBACK (on_emergency_numbers_changed), + self); + phosh_emergency_calls_manager_update (self); +} + + +static void +phosh_emergency_calls_manager_idle_init (PhoshManager *manager) +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (manager); + + /* Connect to call's emergency call DBus interface */ + phosh_dbus_emergency_calls_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + CALLS_BUS_NAME, + CALLS_OBJECT_PATH, + self->cancel, + on_proxy_new_finish, + self); +} + + +static void +emergency_calls_manager_dispose (GObject *object) +{ + PhoshEmergencyCallsManager *self = PHOSH_EMERGENCY_CALLS_MANAGER (object); + + g_action_map_remove_action (G_ACTION_MAP (phosh_shell_get_default ()), + "emergency.toggle-menu"); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->dbus_proxy); + g_clear_object (&self->emergency_contacts); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_emergency_calls_manager_parent_class)->dispose (object); +} + + +static void +phosh_emergency_calls_manager_class_init (PhoshEmergencyCallsManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->get_property = emergency_calls_manager_get_property; + object_class->dispose = emergency_calls_manager_dispose; + + manager_class->idle_init = phosh_emergency_calls_manager_idle_init; + + /** + * PhoshEmergencyCalls:emergency-contacts: + * + * The [type@EmergencyContact]s + */ + props[PROP_EMERGENCY_CONTACTS] = + g_param_spec_object ("emergency-contacts", "", "", + G_TYPE_LIST_STORE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * PhoshEmergencyCalls::dial-error + * + * Failure to dial emergency call number. + */ + signals[DIAL_ERROR] = g_signal_new ("dial-error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_ERROR); +} + + +static GActionEntry entries[] = { + { .name = "emergency.toggle-menu", .activate = on_emergency_menu_activated }, +}; + + +static void +phosh_emergency_calls_manager_init (PhoshEmergencyCallsManager *self) +{ + self->emergency_contacts = g_list_store_new (PHOSH_TYPE_EMERGENCY_CONTACT); + self->cancel = g_cancellable_new (); + /* Force initial sync */ + self->enabled = -1; + + self->settings = g_settings_new (EMERGENCY_CALLS_SETTINGS); + + g_action_map_add_action_entries (G_ACTION_MAP (phosh_shell_get_default ()), + entries, + G_N_ELEMENTS (entries), + self); + /* Disable until we connected to GNOME calls */ + phosh_emergency_calls_manager_set_if_enabled (self, FALSE); +} + + +PhoshEmergencyCallsManager * +phosh_emergency_calls_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_EMERGENCY_CALLS_MANAGER, NULL); +} + + +/** + * phosh_emergency_calls_manager_get_list_store: + * @self: The #PhoshEmergencyCallsManager + * + * Gets the `GListStore` that contains the currently valid emergency contacts. + * + * Returns: (transfer none): List of emergency contacts. + */ +GListStore * +phosh_emergency_calls_manager_get_list_store (PhoshEmergencyCallsManager *self) +{ + g_return_val_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self), NULL); + + return self->emergency_contacts; +} + + +/** + * phosh_emergency_calls_manager_call: + * @self: The EmergencyCallsManager to use to make the call. + * @id: The id that identifies the emergency contact. + * + * Starts an emergency call with the specified id. + */ +void +phosh_emergency_calls_manager_call (PhoshEmergencyCallsManager *self, + const char *id) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (self)); + + g_debug ("Calling emergency contact ID: '%s'", id); + phosh_dbus_emergency_calls_call_call_emergency_contact (self->dbus_proxy, + id, + NULL, + on_call_emergency_contact_finish, + self); +} diff --git a/src/emergency-calls-manager.h b/src/emergency-calls-manager.h new file mode 100644 index 000000000..7063e52a9 --- /dev/null +++ b/src/emergency-calls-manager.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#pragma once + +#include "call.h" +#include "manager.h" + +#include + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_EMERGENCY_CALLS_MANAGER (phosh_emergency_calls_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyCallsManager, phosh_emergency_calls_manager, PHOSH, EMERGENCY_CALLS_MANAGER, PhoshManager) + +PhoshEmergencyCallsManager *phosh_emergency_calls_manager_new (void); +GListStore *phosh_emergency_calls_manager_get_list_store (PhoshEmergencyCallsManager *self); +void phosh_emergency_calls_manager_call (PhoshEmergencyCallsManager *self, const char *id); + +G_END_DECLS diff --git a/src/emergency-contact-row.c b/src/emergency-contact-row.c new file mode 100644 index 000000000..184c3af43 --- /dev/null +++ b/src/emergency-contact-row.c @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#define G_LOG_DOMAIN "phosh-emergency-contact-row" + +#include "phosh-config.h" + +#include "emergency-contact-row.h" +#include "emergency-contact.h" +#include "emergency-calls-manager.h" +#include "util.h" + +#include +#include + + +/** + * PhoshEmergencyContactRow: + * + * A widget that displays a the data in the attached #PhoshEmergencyContact. + * + * contact: The contact object contains data about the bound emergency contact. + * + * #PhoshEmergencyContactRow is a widget derived from `GtkListBoxRow`. It displays the data from + * the bound #PhoshEmergencyContact. + * + * #PhoshEmergencyContactRow is designed to be used with `gtk_list_box_bind_model`. + * + * NOTE: Don't get confused between the widgets on this object and the + * properties on the contact. The widgets are bound to the data on + * the contact object. + */ +struct _PhoshEmergencyContactRow { + HdyActionRow parent; + + GtkLabel *name_label; + GtkLabel *id_label; + + PhoshEmergencyContact *contact; +}; + +G_DEFINE_TYPE (PhoshEmergencyContactRow, phosh_emergency_contact_row, HDY_TYPE_ACTION_ROW) + + +enum { + PROP_0, + PROP_CONTACT, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +static void +bind_contact (PhoshEmergencyContactRow *self) +{ + g_object_bind_property (self->contact, "name", + self, "title", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (self->contact, "id", + self, "subtitle", + G_BINDING_SYNC_CREATE); +} + + +static void +emergency_contact_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyContactRow *self = PHOSH_EMERGENCY_CONTACT_ROW (object); + + switch (property_id) { + case PROP_CONTACT: + g_value_set_object (value, self->contact); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + + +static void +emergency_contact_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyContactRow *self = PHOSH_EMERGENCY_CONTACT_ROW (object); + + switch (property_id) { + case PROP_CONTACT: + g_set_object (&self->contact, g_value_get_object (value)); + bind_contact (self); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +emergency_contact_row_dispose (GObject *object) +{ + PhoshEmergencyContactRow *self = PHOSH_EMERGENCY_CONTACT_ROW (object); + + g_clear_object (&self->contact); + + G_OBJECT_CLASS (phosh_emergency_contact_row_parent_class)->dispose (object); +} + + +static void +phosh_emergency_contact_row_class_init (PhoshEmergencyContactRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = emergency_contact_row_get_property; + object_class->set_property = emergency_contact_row_set_property; + object_class->dispose = emergency_contact_row_dispose; + + /** + * PhoshEmergencyContactRow:contact: + * + * The contact object that contains data about the emergency contact. + */ + props[PROP_CONTACT] = + g_param_spec_object ("contact", "", "", + PHOSH_TYPE_EMERGENCY_CONTACT, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/emergency-contact-row.ui"); +} + + +static void +phosh_emergency_contact_row_init (PhoshEmergencyContactRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +/** + * phosh_emergency_contact_row_new: + * @contact: (transfer none): The emergency contact that the widget will be bound to. + * + * Creates a new emergency contact row bound to [type@EmergencyContact]. + + * Returns: (transfer full): a new #PhoshEmergencyContactRow + */ +PhoshEmergencyContactRow * +phosh_emergency_contact_row_new (PhoshEmergencyContact *contact) +{ + return g_object_new (PHOSH_TYPE_EMERGENCY_CONTACT_ROW, "contact", contact, NULL); +} + + +/** + * phosh_emergency_contact_row_call: + * @self: (transfer none): The EmergencyContactRow that will be called + * @manager: (transfer none): The contact manager that will interface with the API. + * + * Starts an emergency call to the bound emergency contact. + */ +void +phosh_emergency_contact_row_call (PhoshEmergencyContactRow *self, + PhoshEmergencyCallsManager *manager) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CONTACT_ROW (self)); + + phosh_emergency_contact_call (self->contact, manager); +} diff --git a/src/emergency-contact-row.h b/src/emergency-contact-row.h new file mode 100644 index 000000000..0616d34bf --- /dev/null +++ b/src/emergency-contact-row.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#pragma once + +#include "emergency-contact.h" +#include "emergency-calls-manager.h" + +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_CONTACT_ROW (phosh_emergency_contact_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyContactRow, phosh_emergency_contact_row, PHOSH, EMERGENCY_CONTACT_ROW, HdyActionRow) + +PhoshEmergencyContactRow *phosh_emergency_contact_row_new (PhoshEmergencyContact *contact); +void phosh_emergency_contact_row_call (PhoshEmergencyContactRow *self, + PhoshEmergencyCallsManager *manager); + +G_END_DECLS diff --git a/src/emergency-contact.c b/src/emergency-contact.c new file mode 100644 index 000000000..974711f90 --- /dev/null +++ b/src/emergency-contact.c @@ -0,0 +1,222 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#define G_LOG_DOMAIN "phosh-emergency-contact" + +#include "phosh-config.h" + +#include "emergency-contact.h" + +enum { + PROP_0, + PROP_ID, + PROP_NAME, + PROP_SOURCE, + PROP_ADDITIONAL_PROPERTIES, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + +/** + * PhoshEmergencyContact: + * @short_description: A object to hold data about an emergency contact. + * @Title: PhoshEmergencyContact + * + * id: The id that is given to this emergency contact by the calls DBus API. Eg `+123 123 123` + * name: The name of person in this emergency contact. Eg Bob + * source: An integer identifying the source of the emergency contact. Eg SIM card or user entered. + * additional_properties: Any other information, given by the calls DBus API. Format is `a{sv}` + * + * #PhoshEmergencyContact holds data about an emergency contact and is used to build #PhoshEmergencyContactRow. + * + * A GListStore of #PhoshEmergencyContact is provided by #PhoshEmergencyContactManager. + */ +struct _PhoshEmergencyContact { + GObject parent; + + char *id; + char *name; + gint32 source; + GVariant *additional_properties; +}; + +G_DEFINE_TYPE (PhoshEmergencyContact, phosh_emergency_contact, G_TYPE_OBJECT); + +static void +emergency_contact_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyContact *self = PHOSH_EMERGENCY_CONTACT (object); + + switch (property_id) { + case PROP_ID: + g_value_set_string (value, self->id); + break; + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_SOURCE: + g_value_set_int (value, self->source); + break; + case PROP_ADDITIONAL_PROPERTIES: + g_value_set_variant (value, self->additional_properties); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +emergency_contact_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEmergencyContact *self = PHOSH_EMERGENCY_CONTACT (object); + + switch (property_id) { + case PROP_ID: + g_free (self->id); + self->id = g_value_dup_string (value); + break; + case PROP_NAME: + g_free (self->name); + self->name = g_value_dup_string (value); + break; + case PROP_SOURCE: + self->source = g_value_get_int (value); + break; + case PROP_ADDITIONAL_PROPERTIES: + g_clear_pointer (&self->additional_properties, g_variant_unref); + self->additional_properties = g_value_dup_variant (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +emergency_contact_finalize (GObject *object) +{ + PhoshEmergencyContact *self = PHOSH_EMERGENCY_CONTACT (object); + + g_free (self->id); + g_free (self->name); + g_variant_unref (self->additional_properties); + + G_OBJECT_CLASS (phosh_emergency_contact_parent_class)->finalize (object); +} + + +static void +phosh_emergency_contact_class_init (PhoshEmergencyContactClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = emergency_contact_finalize; + object_class->get_property = emergency_contact_get_property; + object_class->set_property = emergency_contact_set_property; + + /** + * PhoshEmergencyContact:id: + * + * The id that is given to this emergency contact by the calls DBus + * API. + */ + props[PROP_ID] = + g_param_spec_string ("id", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshEmergencyContact:name: + * + * The name of person in this emergency contact. Eg Bob. + */ + props[PROP_NAME] = + g_param_spec_string ("name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshEmergencyContact:source: + * + * An integer identifying the source of the emergency contact. Eg SIM card or user entered. + */ + props[PROP_SOURCE] = + g_param_spec_int ("source", "", "", + G_MININT, G_MAXINT, + 1, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshEmergencyContact:additional-properties: + * + * Any other information, given by the calls DBus API. + */ + props[PROP_ADDITIONAL_PROPERTIES] = + g_param_spec_variant ("additional-properties", "", "", + G_VARIANT_TYPE ("a{sv}"), + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + + +static void +phosh_emergency_contact_init (PhoshEmergencyContact *self) +{ +} + +/** + * phosh_emergency_contact_new: + * @id: The id that is given to this emergency contact by the calls DBus API. Eg `+123 123 123` + * @name: The name of person in this emergency contact. Eg Bob + * @source: An integer identifying the source of the emergency contact. Eg SIM card or user entered. + * @additional_properties: Any other information, given by the calls DBus API. Format is `a{sv}` + * + * #phosh_emergency_contact_new creates a new #PhoshEmergencyContact with the provided information. + * The input information is designed parsed from the DBus API and then provided to this function. + * See #PhoshEmergencyContactManager `on_update_finish` function for reference. + */ +PhoshEmergencyContact * +phosh_emergency_contact_new (const char *id, + const char *name, + gint32 source, + GVariant *additional_properties) +{ + return g_object_new (PHOSH_TYPE_EMERGENCY_CONTACT, + "id", id, + "name", name, + "source", source, + "additional-properties", additional_properties, + NULL); +} + +/** + * phosh_emergency_contact_call: + * @self: The Contact to call. + * @emergency_calls_manager: The contact manager that will interface with the API. + * + * Starts an emergency call to this emergency contact. + * + * This function calls #phosh_emergency_contact_manager_call with the + * passed emergency_contact_manager parameter and id from self. + */ +void +phosh_emergency_contact_call (PhoshEmergencyContact *self, + PhoshEmergencyCallsManager *emergency_contact_manager) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_CONTACT (self)); + + phosh_emergency_calls_manager_call (emergency_contact_manager, self->id); +} diff --git a/src/emergency-contact.h b/src/emergency-contact.h new file mode 100644 index 000000000..1fd762ecb --- /dev/null +++ b/src/emergency-contact.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#pragma once + +#include "emergency-calls-manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_CONTACT (phosh_emergency_contact_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyContact, phosh_emergency_contact, PHOSH, EMERGENCY_CONTACT, GObject) + +PhoshEmergencyContact *phosh_emergency_contact_new (const char *id, + const char *name, + gint32 source, + GVariant *additional_properties); +void phosh_emergency_contact_call (PhoshEmergencyContact *self, + PhoshEmergencyCallsManager *emergency_calls_manager); + +G_END_DECLS diff --git a/src/emergency-menu.c b/src/emergency-menu.c new file mode 100644 index 000000000..9dd32b62c --- /dev/null +++ b/src/emergency-menu.c @@ -0,0 +1,272 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#define G_LOG_DOMAIN "phosh-emergency-menu" + +#include "phosh-config.h" + +#include "emergency-menu.h" +#include "emergency-calls-manager.h" +#include "emergency-contact-row.h" +#include "system-modal-dialog.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include + + +/** + * PhoshEmergencyMenu: + * + * A menu that allows the user to dial an emergency service, see + * emergency info and quickly call emergency contacts. + * + * #PhoshEmergencyMenu is a menu that allows the user to dial an + * emergency service, see emergency info and quickly call emergency + * contacts. + * + * The #PhoshEmergencyMenu is designed to be integrated with the lock + * screen via an emergency button. + * + * The fetching of emergency contact and calling of emergency + * contact/service is done via a DBus API with the Calls app, it is + * managed by the #PhoshEmergencyContactManager class. + * + * The emergency contacts list bind a GListStore provided by the + * #PhoshEmergencyContactManager to a `GtkListBox` using + * `gtk_list_box_bind_model` function. + */ + +enum { + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +struct _PhoshEmergencyMenu { + PhoshSystemModalDialog parent; + + PhoshEmergencyCallsManager *manager; + + HdyCarousel *emergency_carousel; + GtkBox *emergency_dialpad_box; + GtkBox *emergency_info_box; + GtkListBox *emergency_contacts_list_box; + GtkLabel *emergency_owner_name; + GtkWidget *placeholder; + + GSimpleActionGroup *actions; +}; +G_DEFINE_TYPE (PhoshEmergencyMenu, phosh_emergency_menu, PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + + +static void +on_go_back_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshEmergencyMenu *self = PHOSH_EMERGENCY_MENU (data); + gboolean close; + + close = g_variant_get_boolean (param); + if (close) { + g_signal_emit (self, signals[DONE], 0); + } else { + hdy_carousel_scroll_to (self->emergency_carousel, GTK_WIDGET (self->emergency_dialpad_box)); + } +} + + +static void +on_emergency_menu_done (PhoshEmergencyMenu *self) +{ + g_return_if_fail (PHOSH_IS_EMERGENCY_MENU (self)); + g_signal_emit (self, signals[DONE], 0); +} + + +static void +on_dial_error (PhoshEmergencyMenu *self, GError *error) +{ + PhoshSystemModalDialog *error_dialog = PHOSH_SYSTEM_MODAL_DIALOG (phosh_system_modal_dialog_new ()); + GtkWidget *error_label = gtk_label_new (NULL); + GtkWidget *ok_button = gtk_button_new_with_label (_("Ok")); + + phosh_system_modal_dialog_add_button (error_dialog, ok_button, -1); + phosh_system_modal_dialog_set_title (error_dialog, _("Unable to place emergency call")); + + if (g_dbus_error_strip_remote_error (error)) + gtk_label_set_label (GTK_LABEL (error_label), error->message); + else + gtk_label_set_label (GTK_LABEL (error_label), _("Internal error")); + gtk_label_set_line_wrap (GTK_LABEL (error_label), TRUE); + phosh_system_modal_dialog_set_content (error_dialog, error_label); + + g_signal_connect_swapped (ok_button, "clicked", G_CALLBACK (gtk_widget_destroy), error_dialog); + g_signal_connect_swapped (error_dialog, + "dialog-canceled", + G_CALLBACK (gtk_widget_destroy), + error_dialog); + + gtk_widget_set_visible (error_label, TRUE); + gtk_widget_set_visible (ok_button, TRUE); + gtk_widget_set_visible (GTK_WIDGET (error_dialog), TRUE); +} + + +static GtkWidget * +create_emergency_contact_row (gpointer item, gpointer unused) +{ + PhoshEmergencyContact *contact = PHOSH_EMERGENCY_CONTACT (item); + + return GTK_WIDGET (phosh_emergency_contact_row_new (contact)); +} + + +static void +on_n_items_changed (PhoshEmergencyMenu *self, GParamSpec *pspec, GListStore *store) +{ + gboolean visible; + + g_return_if_fail (PHOSH_IS_EMERGENCY_MENU (self)); + g_return_if_fail (G_IS_LIST_STORE (store)); + + /* Work around https://gitlab.gnome.org/GNOME/libhandy/-/issues/468 */ + visible = !g_list_model_get_n_items (G_LIST_MODEL (store)); + gtk_widget_set_visible (self->placeholder, visible); +} + + + +static void +emergency_menu_constructed (GObject *object) +{ + PhoshEmergencyMenu *self = PHOSH_EMERGENCY_MENU (object); + GListStore *emergency_contacts_list; + + G_OBJECT_CLASS (phosh_emergency_menu_parent_class)->constructed (object); + + self->manager = phosh_shell_get_emergency_calls_manager (phosh_shell_get_default ()); + + emergency_contacts_list = phosh_emergency_calls_manager_get_list_store (self->manager); + gtk_list_box_bind_model (self->emergency_contacts_list_box, + G_LIST_MODEL (emergency_contacts_list), + create_emergency_contact_row, + NULL, + NULL); + g_signal_connect_object (emergency_contacts_list, "notify::n-items", + G_CALLBACK (on_n_items_changed), + self, + G_CONNECT_SWAPPED); + on_n_items_changed (self, NULL, emergency_contacts_list); + + gtk_label_set_label (self->emergency_owner_name, g_get_real_name ()); + g_signal_connect_swapped (self->manager, + "dial-error", + G_CALLBACK (on_dial_error), + self); +} + + +static void +emergency_menu_dispose (GObject *object) +{ + PhoshEmergencyMenu *self = PHOSH_EMERGENCY_MENU (object); + + g_clear_object (&self->actions); + + G_OBJECT_CLASS (phosh_emergency_menu_parent_class)->dispose (object); +} + +static void +on_dialpad_dialed (PhoshEmergencyMenu *self, const char *number) +{ + phosh_emergency_calls_manager_call (self->manager, number); +} + +/** + * on_emergency_contacts_list_box_activated: + * + * When the user clicks on an emergency contact call that contact via + * the phosh_emergency_calls_row_call function. + */ +static void +on_emergency_contacts_list_box_activated (PhoshEmergencyMenu *self, PhoshEmergencyContactRow* row, GtkListBox* list) +{ + phosh_emergency_contact_row_call (row, self->manager); +} + +/** + * emergency_contacts_button_pressed_cb: + * + * When the "Emergency Info" button on the dial pad screen is pressed + * send the user to the info screen. + */ +static void +on_emergency_contacts_button_clicked (PhoshEmergencyMenu *self) +{ + g_debug ("Emergency info button pressed"); + hdy_carousel_scroll_to (self->emergency_carousel, GTK_WIDGET (self->emergency_info_box)); +} + + +static void +phosh_emergency_menu_class_init (PhoshEmergencyMenuClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = emergency_menu_constructed; + object_class->dispose = emergency_menu_dispose; + + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/emergency-menu.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, emergency_carousel); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, emergency_dialpad_box); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, emergency_info_box); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, emergency_contacts_list_box); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, emergency_owner_name); + gtk_widget_class_bind_template_child (widget_class, PhoshEmergencyMenu, placeholder); + gtk_widget_class_bind_template_callback (widget_class, on_emergency_contacts_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_emergency_contacts_list_box_activated); + gtk_widget_class_bind_template_callback (widget_class, on_dialpad_dialed); + gtk_widget_class_bind_template_callback (widget_class, on_emergency_menu_done); + + gtk_widget_class_set_css_name (widget_class, "phosh-emergency-menu"); +} + + +static GActionEntry entries[] = { + { .name = "go-back", .parameter_type = "b", .activate = on_go_back_activated, .state = "1" }, +}; + + +static void +phosh_emergency_menu_init (PhoshEmergencyMenu *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->actions = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->actions), + entries, G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), "emergency-menu", + G_ACTION_GROUP (self->actions)); +} + + +PhoshEmergencyMenu * +phosh_emergency_menu_new (void) +{ + return g_object_new (PHOSH_TYPE_EMERGENCY_MENU, NULL); +} diff --git a/src/emergency-menu.h b/src/emergency-menu.h new file mode 100644 index 000000000..aa755ed1e --- /dev/null +++ b/src/emergency-menu.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Thomas Booker + */ + +#pragma once + +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_EMERGENCY_MENU (phosh_emergency_menu_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEmergencyMenu, phosh_emergency_menu, PHOSH, EMERGENCY_MENU, PhoshSystemModalDialog) + +PhoshEmergencyMenu *phosh_emergency_menu_new (void); + +G_END_DECLS diff --git a/src/end-session-dialog.c b/src/end-session-dialog.c new file mode 100644 index 000000000..474ecfa45 --- /dev/null +++ b/src/end-session-dialog.c @@ -0,0 +1,586 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Parts taken from gnome-flashback which is: + * + * Copyright (C) 2008 William Jon McCann + * Copyright (C) 2015 Alberts Muktupāvels + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-end-session-dialog" + +#include "phosh-config.h" +#include "end-session-dialog.h" +#include "util.h" + +#include + +#include +#include + +#define SYNC_DBUS_TIMEOUT 500 + +/** + * PhoshEndSessionDialog: + * + * A system modal prompt to authorize applications + * + * The #PhoshEndSessionDialog is used to confirm/decline the end of the session + * and is spawned by the #PhoshSessionManager. + */ + +/* Taken from gnome-session/gsm-inhibitor-flag.h */ +typedef enum { + PHOSH_GSM_INHIBITOR_FLAG_LOGOUT = 1 << 0, + PHOSH_GSM_INHIBITOR_FLAG_SWITCH_USER = 1 << 1, + PHOSH_GSM_INHIBITOR_FLAG_SUSPEND = 1 << 2, + PHOSH_GSM_INHIBITOR_FLAG_IDLE = 1 << 3, + PHOSH_GSM_INHIBITOR_FLAG_AUTOMOUNT = 1 << 4 +} PhoshGsmInhibitorFlags; + +enum { + CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +enum { + PROP_0, + PROP_ACTION, + PROP_TIMEOUT, + PROP_INHIBITOR_PATHS, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +static void end_session_dialog_update (PhoshEndSessionDialog *self); + +typedef struct _PhoshEndSessionDialog { + PhoshSystemModalDialog parent; + + int action; + gboolean action_confirmed; + int timeout; + int timeout_id; + GStrv inhibitor_paths; + + GtkWidget *lbl_subtitle; + GtkWidget *lbl_warn; + GtkWidget *listbox; + GtkWidget *sw_inhibitors; + GtkWidget *btn_confirm; + GtkWidget *btn_cancel; + + GCancellable *cancel; +} PhoshEndSessionDialog; + + +G_DEFINE_TYPE (PhoshEndSessionDialog, phosh_end_session_dialog, PHOSH_TYPE_SYSTEM_MODAL_DIALOG) + +static gboolean +is_inhibited (PhoshEndSessionDialog *self) +{ + return (self->inhibitor_paths && g_strv_length (self->inhibitor_paths)); +} + + +static char * +get_user_name (void) +{ + char *name; + + name = g_locale_to_utf8 (g_get_real_name (), -1, NULL, NULL, NULL); + + if (g_strcmp0 (name, "Unknown") == 0 || g_strcmp0 (name, "") == 0) { + g_free (name); + name = g_locale_to_utf8 (g_get_user_name (), -1, NULL, NULL, NULL); + } + + if (!name) + name = g_strdup (g_get_user_name ()); + + return name; +} + + +static void +on_btn_confirm_clicked (PhoshEndSessionDialog *self, GtkButton *btn) +{ + self->action_confirmed = TRUE; + + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +on_dialog_canceled (PhoshEndSessionDialog *self) +{ + g_return_if_fail (PHOSH_IS_END_SESSION_DIALOG (self)); + + g_signal_emit (self, signals[CLOSED], 0); +} + + +static gboolean +end_session_dialog_timeout (gpointer data) +{ + PhoshEndSessionDialog *self = PHOSH_END_SESSION_DIALOG (data); + + self->timeout--; + end_session_dialog_update (self); + + if (self->timeout) + return G_SOURCE_CONTINUE; + + on_btn_confirm_clicked (self, GTK_BUTTON (self->btn_confirm)); + self->timeout_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +maybe_start_timer (PhoshEndSessionDialog *self) +{ + if (self->timeout_id == 0 && self->timeout) { + self->timeout_id = g_timeout_add_seconds (1, end_session_dialog_timeout, self); + g_source_set_name_by_id (self->timeout_id, "[phosh] end_session_dialog_timeout"); + } +} + + +static void +end_session_dialog_update (PhoshEndSessionDialog *self) +{ + gboolean inhibited; + gint seconds; + const char *title; + g_autofree char *description = NULL; + g_autofree char *user_name = NULL; + + maybe_start_timer (self); + seconds = self->timeout; + inhibited = is_inhibited (self); + + g_debug ("Action: %d, seconds: %d, inhibit: %d", self->action, seconds, inhibited); + + switch (self->action) { + case PHOSH_END_SESSION_ACTION_LOGOUT: + title = _("Log Out"); + + user_name = get_user_name (); + description = g_strdup_printf (ngettext ("%s will be logged out automatically in %d second.", + "%s will be logged out automatically in %d seconds.", + seconds), + user_name, seconds); + break; + case PHOSH_END_SESSION_ACTION_SHUTDOWN: + title = _("Power Off"); + description = g_strdup_printf (ngettext ("The system will power off automatically in %d second.", + "The system will power off automatically in %d seconds.", + seconds), + seconds); + break; + case PHOSH_END_SESSION_ACTION_REBOOT: + title = _("Restart"); + description = g_strdup_printf (ngettext ("The system will restart automatically in %d second.", + "The system will restart automatically in %d seconds.", + seconds), + seconds); + break; + default: + g_return_if_reached (); + } + + phosh_system_modal_dialog_set_title (PHOSH_SYSTEM_MODAL_DIALOG (self), title); + + gtk_label_set_label (GTK_LABEL (self->lbl_subtitle), description); + gtk_button_set_label (GTK_BUTTON (self->btn_confirm), title); +} + + +static char * +inhibitor_get_app_id (GDBusProxy *proxy) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) res = NULL; + char *app_id; + + res = g_dbus_proxy_call_sync (proxy, "GetAppId", NULL, + 0, SYNC_DBUS_TIMEOUT, NULL, &error); + if (!res) { + g_warning ("Failed to get Inhibitor app id: %s", error->message); + return NULL; + } + g_variant_get (res, "(s)", &app_id); + + return app_id; +} + + +static PhoshGsmInhibitorFlags +inhibitor_get_flags (GDBusProxy *proxy) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) res = NULL; + guint flags; + + res = g_dbus_proxy_call_sync (proxy, "GetFlags", NULL, + 0, SYNC_DBUS_TIMEOUT, NULL, &error); + if (!res) { + g_warning ("Failed to get Inhibitor flags: %s", error->message); + return 0; + } + g_variant_get (res, "(u)", &flags); + + return flags; +} + + +static char * +inhibitor_get_reason (GDBusProxy *proxy) +{ + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) res = NULL; + char *reason; + + res = g_dbus_proxy_call_sync (proxy, "GetReason", NULL, + 0, SYNC_DBUS_TIMEOUT, NULL, &error); + if (!res) { + g_warning ("Failed to get inhibit reason: %s", error->message); + return NULL; + } + g_variant_get (res, "(s)", &reason); + + return reason; +} + + +static void +add_inhibitor (PhoshEndSessionDialog *self, GDBusProxy *inhibitor) +{ + g_autofree char *app_id = NULL; + g_autofree char *reason = NULL; + g_autoptr (GDesktopAppInfo) app_info = NULL; + PhoshGsmInhibitorFlags flags; + const char *icon_name = NULL; + const char *name = NULL; + GIcon *icon = NULL; + GtkWidget *box; + GtkWidget *box_text; + GtkWidget *label; + GtkWidget *lbl_reason; + GtkWidget *img; + + app_id = inhibitor_get_app_id (inhibitor); + reason = inhibitor_get_reason (inhibitor); + flags = inhibitor_get_flags (inhibitor); + + if (!(flags & PHOSH_GSM_INHIBITOR_FLAG_LOGOUT)) + return; + + if (gm_str_is_null_or_empty (app_id)) + return; + + app_info = phosh_get_desktop_app_info_for_app_id (app_id); + if (app_info) { + icon = g_app_info_get_icon (G_APP_INFO (app_info)); + name = g_app_info_get_display_name (G_APP_INFO (app_info)); + } + + if (!name) + name = app_id; + + if (!icon) + icon_name = "app-icon-unknown"; + + img = g_object_new (GTK_TYPE_IMAGE, + "visible", TRUE, + "can-focus", FALSE, + "gicon", icon, + "halign", GTK_ALIGN_START, + "pixel_size", 64, + NULL); + if (!icon) + g_object_set (img, "icon-name", icon_name, NULL); + + box_text = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + "can-focus", FALSE, + "halign", GTK_ALIGN_START, + "homogeneous", TRUE, + "orientation", GTK_ORIENTATION_VERTICAL, + "spacing", 0, + NULL); + + label = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "can-focus", FALSE, + "ellipsize", PANGO_ELLIPSIZE_MIDDLE, + "halign", GTK_ALIGN_START, + "label", name, + "valign", GTK_ALIGN_END, + NULL); + gtk_box_pack_start (GTK_BOX (box_text), label, TRUE, TRUE, 0); + + if (reason) { + lbl_reason = g_object_new (GTK_TYPE_LABEL, + "visible", TRUE, + "can-focus", FALSE, + "ellipsize", PANGO_ELLIPSIZE_MIDDLE, + "halign", GTK_ALIGN_START, + "label", reason, + "valign", GTK_ALIGN_START, + NULL); + gtk_box_pack_end (GTK_BOX (box_text), lbl_reason, TRUE, TRUE, 0); + } else { + gtk_widget_set_valign (label, GTK_ALIGN_FILL); + } + + box = g_object_new (GTK_TYPE_BOX, + "visible", TRUE, + "can-focus", FALSE, + "halign", GTK_ALIGN_START, + "orientation", GTK_ORIENTATION_HORIZONTAL, + "spacing", 12, + NULL); + + gtk_box_pack_start (GTK_BOX (box), img, TRUE, TRUE, 0); + gtk_box_pack_end (GTK_BOX (box), box_text, FALSE, FALSE, 0); + + gtk_list_box_insert (GTK_LIST_BOX (self->listbox), GTK_WIDGET (box), -1); + gtk_widget_set_visible (GTK_WIDGET (self->sw_inhibitors), TRUE); +} + + +static void +on_inhibitor_created (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + PhoshEndSessionDialog *self; + g_autoptr (GError) error = NULL; + g_autoptr (GDBusProxy) proxy = NULL; + + proxy = g_dbus_proxy_new_for_bus_finish (res, &error); + + if (!proxy) { + g_warning ("Failed to create Inhibitor proxy: %s", error->message); + return; + } + + self = PHOSH_END_SESSION_DIALOG (user_data); + add_inhibitor (self, proxy); +} + + +static void +clear_inhibitors (PhoshEndSessionDialog *self) +{ + g_autoptr (GList) children = NULL; + + g_return_if_fail (GTK_IS_LIST_BOX (self->listbox)); + + children = gtk_container_get_children (GTK_CONTAINER (self->listbox)); + for (GList *child = children; child; child = child->next) + gtk_container_remove (GTK_CONTAINER (self->listbox), child->data); + + gtk_widget_set_visible (self->sw_inhibitors, FALSE); +} + + +static void +end_session_dialog_update_inhibitors (PhoshEndSessionDialog *self, GStrv paths) +{ + g_strfreev (self->inhibitor_paths); + self->inhibitor_paths = g_strdupv ((char **) paths); + + clear_inhibitors (self); + + if (!self->inhibitor_paths) + return; + + for (int i = 0; self->inhibitor_paths[i]; i++) { + g_dbus_proxy_new_for_bus (G_BUS_TYPE_SESSION, 0, NULL, + "org.gnome.SessionManager", + self->inhibitor_paths[i], + "org.gnome.SessionManager.Inhibitor", + self->cancel, on_inhibitor_created, self); + } +} + + +static void +phosh_end_session_dialog_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshEndSessionDialog *self = PHOSH_END_SESSION_DIALOG (obj); + + switch (prop_id) { + case PROP_ACTION: + self->action = g_value_get_int (value); + end_session_dialog_update (self); + break; + case PROP_TIMEOUT: + self->timeout = g_value_get_int (value); + end_session_dialog_update (self); + break; + case PROP_INHIBITOR_PATHS: + end_session_dialog_update_inhibitors (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_end_session_dialog_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshEndSessionDialog *self = PHOSH_END_SESSION_DIALOG (obj); + + switch (prop_id) { + case PROP_ACTION: + g_value_set_int (value, self->action); + break; + case PROP_TIMEOUT: + g_value_set_int (value, self->timeout); + break; + case PROP_INHIBITOR_PATHS: + g_value_set_boxed (value, self->inhibitor_paths); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_end_session_dialog_dispose (GObject *obj) +{ + PhoshEndSessionDialog *self = PHOSH_END_SESSION_DIALOG (obj); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (self->listbox) + clear_inhibitors (self); + + G_OBJECT_CLASS (phosh_end_session_dialog_parent_class)->dispose (obj); +} + + +static void +phosh_end_session_dialog_finalize (GObject *obj) +{ + PhoshEndSessionDialog *self = PHOSH_END_SESSION_DIALOG (obj); + + g_clear_handle_id (&self->timeout_id, g_source_remove); + g_clear_pointer (&self->inhibitor_paths, g_strfreev); + + G_OBJECT_CLASS (phosh_end_session_dialog_parent_class)->finalize (obj); +} + + +static void +phosh_end_session_dialog_class_init (PhoshEndSessionDialogClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_end_session_dialog_get_property; + object_class->set_property = phosh_end_session_dialog_set_property; + object_class->dispose = phosh_end_session_dialog_dispose; + object_class->finalize = phosh_end_session_dialog_finalize; + + props[PROP_ACTION] = + g_param_spec_int ("action", + "Action", + "The requested action", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_TIMEOUT] = + g_param_spec_int ("timeout", + "Timeout", + "Timeout in seconds after which the action is performed", + -1, + G_MAXINT, + -1, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_INHIBITOR_PATHS] = + g_param_spec_boxed ("inhibitor-paths", + "Inhibitor paths", + "Paths to inhibitors that prevent atction", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/end-session-dialog.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, lbl_subtitle); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, lbl_warn); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, listbox); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, sw_inhibitors); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, btn_confirm); + gtk_widget_class_bind_template_child (widget_class, PhoshEndSessionDialog, btn_cancel); + gtk_widget_class_bind_template_callback (widget_class, on_btn_confirm_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); +} + + +static void +phosh_end_session_dialog_init (PhoshEndSessionDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_end_session_dialog_new (gint action, + gint timeout, + const char *const *inhibitor_paths) +{ + return g_object_new (PHOSH_TYPE_END_SESSION_DIALOG, + "action", action, + "timeout", timeout, + "inhibitor-paths", inhibitor_paths, + NULL); +} + + +gboolean +phosh_end_session_dialog_get_action_confirmed (PhoshEndSessionDialog *self) +{ + g_return_val_if_fail (PHOSH_END_SESSION_DIALOG (self), FALSE); + + return self->action_confirmed; +} + + +gboolean +phosh_end_session_dialog_get_action (PhoshEndSessionDialog *self) +{ + g_return_val_if_fail (PHOSH_END_SESSION_DIALOG (self), 0); + + return self->action; +} diff --git a/src/end-session-dialog.h b/src/end-session-dialog.h new file mode 100644 index 000000000..39f81eb6c --- /dev/null +++ b/src/end-session-dialog.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_END_SESSION_DIALOG (phosh_end_session_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshEndSessionDialog, phosh_end_session_dialog, PHOSH, + END_SESSION_DIALOG, PhoshSystemModalDialog) + +/** + * PhoshLogoutAction: + * @PHOSH_END_SESSION_ACTION_LOGOUT: Loguout + * @PHOSH_END_SESSION_ACTION_SHUTDOWN: Shutdown + * @PHOSH_END_SESSION_ACTION_REBOOT: Reboot + * + * The requested action the #PhoshEndSessionDialog should display. This matches + * the values of the DBus protocols 'open' request.. + */ +typedef enum { + PHOSH_END_SESSION_ACTION_LOGOUT, + PHOSH_END_SESSION_ACTION_SHUTDOWN, + PHOSH_END_SESSION_ACTION_REBOOT, + /* Not used by gnome-session */ + /**/ + PHOSH_END_SESSION_ACTION_HIBERNATE, + PHOSH_END_SESSION_ACTION_SUSPEND, + PHOSH_END_SESSION_ACTION_HYBRID_SLEEP, +} PhoshLogoutAction; + +GtkWidget *phosh_end_session_dialog_new (gint action, + gint seconds, + const char *const * paths); +gboolean phosh_end_session_dialog_get_action_confirmed (PhoshEndSessionDialog *self); +gint phosh_end_session_dialog_get_action (PhoshEndSessionDialog *self); + +G_END_DECLS diff --git a/src/fader.c b/src/fader.c new file mode 100644 index 000000000..d7efcd14d --- /dev/null +++ b/src/fader.c @@ -0,0 +1,268 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-fader" + +#include "phosh-config.h" +#include "animation.h" +#include "fader.h" +#include "layersurface-priv.h" +#include "shell-priv.h" + +#include + +/** + * PhoshFader: + * + * A fader + * + * A fullsreen surface that fades in or out. + */ + +#define PHOSH_FADER_DEFAULT_STYLE_CLASS "phosh-fader-default-fade" + +enum { + PROP_0, + PROP_MONITOR, + PROP_STYLE_CLASS, + PROP_FADE_OUT_TIME, + PROP_FADE_OUT_TYPE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshFader +{ + PhoshLayerSurface parent; + PhoshMonitor *monitor; + char *style_class; + + PhoshAnimation *fadeout; + guint fade_out_time; + PhoshAnimationType fade_out_type; +} PhoshFader; + +G_DEFINE_TYPE (PhoshFader, phosh_fader, PHOSH_TYPE_LAYER_SURFACE) + + +static void +fadeout_value_cb (double value, PhoshFader *self) +{ + phosh_layer_surface_set_alpha (PHOSH_LAYER_SURFACE (self), 1.0 - value); +} + + +static void +fadeout_done_cb (GtkWidget *self) +{ + gtk_widget_destroy (self); +} + + +static void +phosh_fader_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFader *self = PHOSH_FADER (obj); + + switch (prop_id) { + case PROP_MONITOR: + g_set_object (&self->monitor, g_value_get_object (value)); + break; + case PROP_FADE_OUT_TIME: + self->fade_out_time = g_value_get_uint (value); + break; + case PROP_FADE_OUT_TYPE: + self->fade_out_type = g_value_get_enum (value); + break; + case PROP_STYLE_CLASS: + g_free (self->style_class); + self->style_class = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_fader_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFader *self = PHOSH_FADER (obj); + + switch (prop_id) { + case PROP_MONITOR: + g_value_set_object (value, self->monitor); + break; + case PROP_FADE_OUT_TIME: + g_value_set_uint (value, self->fade_out_time); + break; + case PROP_FADE_OUT_TYPE: + g_value_set_enum (value, self->fade_out_type); + break; + case PROP_STYLE_CLASS: + g_value_set_string (value, self->style_class); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_fader_show (GtkWidget *widget) +{ + PhoshFader *self = PHOSH_FADER (widget); + gboolean enable_animations; + GtkStyleContext *context; + + enable_animations = hdy_get_enable_animations (widget); + + if (enable_animations) { + const char *style_class; + + style_class = self->style_class ?: PHOSH_FADER_DEFAULT_STYLE_CLASS; + context = gtk_widget_get_style_context (widget); + gtk_style_context_add_class (context, style_class); + } + + GTK_WIDGET_CLASS (phosh_fader_parent_class)->show (widget); +} + + +static void +phosh_fader_dispose (GObject *object) +{ + PhoshFader *self = PHOSH_FADER (object); + + g_clear_pointer (&self->fadeout, phosh_animation_unref); + g_clear_object (&self->monitor); + g_clear_pointer (&self->style_class, g_free); + + G_OBJECT_CLASS (phosh_fader_parent_class)->dispose (object); +} + + +static void +phosh_fader_constructed (GObject *object) +{ + PhoshFader *self = PHOSH_FADER (object); + PhoshWayland *wl = phosh_wayland_get_default (); + + if (self->monitor == NULL) + self->monitor = g_object_ref (phosh_shell_get_primary_monitor (phosh_shell_get_default ())); + + g_object_set (PHOSH_LAYER_SURFACE (self), + "layer-shell", phosh_wayland_get_zwlr_layer_shell_v1 (wl), + "wl-output", phosh_monitor_get_wl_output (self->monitor), + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", FALSE, + "exclusive-zone", -1, + "namespace", "phosh fader", + NULL); + + G_OBJECT_CLASS (phosh_fader_parent_class)->constructed (object); +} + + +static void +phosh_fader_class_init (PhoshFaderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_fader_get_property; + object_class->set_property = phosh_fader_set_property; + object_class->constructed = phosh_fader_constructed; + object_class->dispose = phosh_fader_dispose; + widget_class->show = phosh_fader_show; + + props[PROP_MONITOR] = + g_param_spec_object ("monitor", "", "", + PHOSH_TYPE_MONITOR, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshFader:fade-out-time: + * + * The time of a fade out animation in ms. 0 to enable any + * fade out. + */ + props[PROP_FADE_OUT_TIME] = + g_param_spec_uint ("fade-out-time", "", "", + 0, G_MAXUINT, 0, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshFader:fade-out-type: + * + * The animation type for the fade out. + */ + props[PROP_FADE_OUT_TYPE] = + g_param_spec_enum ("fade-out-type", "", "", + PHOSH_TYPE_ANIMATION_TYPE, + PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshFader:style-class + * + * The CSS style class used for the animation + */ + props[PROP_STYLE_CLASS] = + g_param_spec_string ("style-class", "", "", + NULL, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_fader_init (PhoshFader *self) +{ + self->style_class = g_strdup (PHOSH_FADER_DEFAULT_STYLE_CLASS); +} + + +PhoshFader * +phosh_fader_new (PhoshMonitor *monitor) +{ + return g_object_new (PHOSH_TYPE_FADER, "monitor", monitor, NULL); +} + + +void +phosh_fader_hide (PhoshFader *self) +{ + g_return_if_fail (PHOSH_IS_FADER (self)); + + if (self->fade_out_time == 0) { + gtk_widget_destroy (GTK_WIDGET (self)); + return; + } + + self->fadeout = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + self->fade_out_time * PHOSH_ANIMATION_SLOWDOWN, + self->fade_out_type, + (PhoshAnimationValueCallback) fadeout_value_cb, + (PhoshAnimationDoneCallback) fadeout_done_cb, + self); + + phosh_animation_start (self->fadeout); +} diff --git a/src/fader.h b/src/fader.h new file mode 100644 index 000000000..9d49ff2e7 --- /dev/null +++ b/src/fader.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "monitor/monitor.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FADER (phosh_fader_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshFader, phosh_fader, PHOSH, FADER, PhoshLayerSurface) + +PhoshFader *phosh_fader_new (PhoshMonitor *monitor); +void phosh_fader_hide (PhoshFader *self); + +G_END_DECLS diff --git a/src/fading-label.c b/src/fading-label.c new file mode 100644 index 000000000..741134bad --- /dev/null +++ b/src/fading-label.c @@ -0,0 +1,327 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + * + * Based on from which is LGPL-2.1+. + */ + +#include "phosh-config.h" +#include "fading-label.h" + +#include +#include "bidi.h" + +/** + * PhoshFadingLabel: + * + * A label that visually fades out when too wide for the given space. + */ + +#define FADE_WIDTH 18 + +struct _PhoshFadingLabel +{ + GtkBin parent_instance; + + GtkWidget *label; + gfloat align; + cairo_pattern_t *gradient; +}; + +G_DEFINE_TYPE (PhoshFadingLabel, phosh_fading_label, GTK_TYPE_BIN) + +enum { + PROP_0, + PROP_LABEL, + PROP_ALIGN, + LAST_PROP +}; + +static GParamSpec *props[LAST_PROP]; + +static gboolean +is_rtl (PhoshFadingLabel *self) +{ + PangoDirection pango_direction = PANGO_DIRECTION_NEUTRAL; + const char *label = phosh_fading_label_get_label (self); + + if (label) + pango_direction = phosh_find_base_dir (label, -1); + + if (pango_direction == PANGO_DIRECTION_RTL) + return TRUE; + + if (pango_direction == PANGO_DIRECTION_LTR) + return FALSE; + + return gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; +} + +static void +ensure_gradient (PhoshFadingLabel *self) +{ + if (self->gradient) + return; + + self->gradient = cairo_pattern_create_linear (0, 0, 1, 0); + cairo_pattern_add_color_stop_rgba (self->gradient, 0, 1, 1, 1, 0); + cairo_pattern_add_color_stop_rgba (self->gradient, 1, 1, 1, 1, 1); +} + +static void +phosh_fading_label_get_preferred_width (GtkWidget *widget, + gint *min, + gint *nat) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (widget); + + gtk_widget_get_preferred_width (self->label, min, nat); + + if (min) + *min = 0; +} + +static void +phosh_fading_label_get_preferred_width_for_height (GtkWidget *widget, + gint for_height, + gint *min, + gint *nat) +{ + phosh_fading_label_get_preferred_width (widget, min, nat); +} + +static void +phosh_fading_label_size_allocate (GtkWidget *widget, + GtkAllocation *allocation) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (widget); + gfloat align = is_rtl (self) ? 1 - self->align : self->align; + GtkAllocation child_allocation; + gint child_width; + + gtk_widget_set_allocation (widget, allocation); + + phosh_fading_label_get_preferred_width (widget, NULL, &child_width); + + child_allocation.x = allocation->x + (gint) ((allocation->width - child_width) * align); + child_allocation.y = allocation->y; + child_allocation.width = child_width; + child_allocation.height = allocation->height; + + gtk_widget_size_allocate (self->label, &child_allocation); + + gtk_widget_get_clip (self->label, &child_allocation); + child_allocation.x = allocation->x; + child_allocation.width = allocation->width; + gtk_widget_set_clip (self->label, &child_allocation); +} + +static gboolean +phosh_fading_label_draw (GtkWidget *widget, + cairo_t *cr) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (widget); + gfloat align = is_rtl (self) ? 1 - self->align : self->align; + GtkAllocation clip, alloc; + int child_width = gtk_widget_get_allocated_width (self->label); + + gtk_widget_get_allocation (widget, &alloc); + + if (child_width <= alloc.width) { + gtk_container_propagate_draw (GTK_CONTAINER (widget), self->label, cr); + + return GDK_EVENT_PROPAGATE; + } + + ensure_gradient (self); + + gtk_widget_get_clip (self->label, &clip); + clip.x = 0; + clip.y -= alloc.y; + clip.width = alloc.width; + + cairo_save (cr); + cairo_rectangle (cr, clip.x, clip.y, clip.width, clip.height); + cairo_clip (cr); + + cairo_push_group (cr); + gtk_container_propagate_draw (GTK_CONTAINER (widget), self->label, cr); + + if (align > 0) { + cairo_save (cr); + cairo_translate (cr, clip.x + FADE_WIDTH, clip.y); + cairo_scale (cr, -FADE_WIDTH, clip.height); + cairo_set_source (cr, self->gradient); + cairo_rectangle (cr, 0, 0, 1, 1); + cairo_set_operator (cr, CAIRO_OPERATOR_DEST_OUT); + cairo_fill (cr); + cairo_restore (cr); + } + + if (align < 1) { + cairo_translate (cr, clip.x + clip.width - FADE_WIDTH, clip.y); + cairo_scale (cr, FADE_WIDTH, clip.height); + cairo_set_source (cr, self->gradient); + cairo_rectangle (cr, 0, 0, 1, 1); + cairo_set_operator (cr, CAIRO_OPERATOR_DEST_OUT); + cairo_fill (cr); + } + + cairo_pop_group_to_source (cr); + cairo_paint (cr); + + cairo_restore (cr); + + return GDK_EVENT_PROPAGATE; +} + +static void +phosh_fading_label_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (object); + + switch (prop_id) { + case PROP_LABEL: + g_value_set_string (value, phosh_fading_label_get_label (self)); + break; + + case PROP_ALIGN: + g_value_set_float (value, phosh_fading_label_get_align (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +phosh_fading_label_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (object); + + switch (prop_id) { + case PROP_LABEL: + phosh_fading_label_set_label (self, g_value_get_string (value)); + break; + + case PROP_ALIGN: + phosh_fading_label_set_align (self, g_value_get_float (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +phosh_fading_label_finalize (GObject *object) +{ + PhoshFadingLabel *self = PHOSH_FADING_LABEL (object); + + g_clear_pointer (&self->gradient, cairo_pattern_destroy); + + G_OBJECT_CLASS (phosh_fading_label_parent_class)->finalize (object); +} + +static void +phosh_fading_label_class_init (PhoshFadingLabelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_fading_label_get_property; + object_class->set_property = phosh_fading_label_set_property; + object_class->finalize = phosh_fading_label_finalize; + + widget_class->get_preferred_width = phosh_fading_label_get_preferred_width; + widget_class->get_preferred_width_for_height = phosh_fading_label_get_preferred_width_for_height; + widget_class->size_allocate = phosh_fading_label_size_allocate; + widget_class->draw = phosh_fading_label_draw; + + props[PROP_LABEL] = + g_param_spec_string ("label", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_ALIGN] = + g_param_spec_float ("align", "", "", + 0.0, 1.0, 0.0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +static void +phosh_fading_label_init (PhoshFadingLabel *self) +{ + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + + self->label = gtk_label_new (NULL); + gtk_widget_set_visible (self->label, TRUE); + gtk_label_set_single_line_mode (GTK_LABEL (self->label), TRUE); + + gtk_container_add (GTK_CONTAINER (self), self->label); +} + +GtkWidget * +phosh_fading_label_new (const char *label) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_FADING_LABEL, "label", label, NULL)); +} + +const char * +phosh_fading_label_get_label (PhoshFadingLabel *self) +{ + g_return_val_if_fail (PHOSH_IS_FADING_LABEL (self), NULL); + + return gtk_label_get_label (GTK_LABEL (self->label)); +} + +void +phosh_fading_label_set_label (PhoshFadingLabel *self, + const char *label) +{ + g_return_if_fail (PHOSH_IS_FADING_LABEL (self)); + + if (!g_strcmp0 (label, phosh_fading_label_get_label (self))) + return; + + gtk_label_set_label (GTK_LABEL (self->label), label); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LABEL]); +} + +float +phosh_fading_label_get_align (PhoshFadingLabel *self) +{ + g_return_val_if_fail (PHOSH_IS_FADING_LABEL (self), 0.0f); + + return self->align; +} + +void +phosh_fading_label_set_align (PhoshFadingLabel *self, + gfloat align) +{ + g_return_if_fail (PHOSH_IS_FADING_LABEL (self)); + + align = CLAMP (align, 0.0, 1.0); + + if (!(self->align > align || self->align < align)) + return; + + self->align = align; + + gtk_widget_queue_allocate (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALIGN]); +} diff --git a/src/fading-label.h b/src/fading-label.h new file mode 100644 index 000000000..1df288bf0 --- /dev/null +++ b/src/fading-label.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FADING_LABEL (phosh_fading_label_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshFadingLabel, phosh_fading_label, PHOSH, FADING_LABEL, GtkBin) + +GtkWidget *phosh_fading_label_new (const char *label); +const char *phosh_fading_label_get_label (PhoshFadingLabel *self); +void phosh_fading_label_set_label (PhoshFadingLabel *self, + const char *label); + +float phosh_fading_label_get_align (PhoshFadingLabel *self); +void phosh_fading_label_set_align (PhoshFadingLabel *self, + gfloat align); + +G_END_DECLS diff --git a/src/fake-clock.c b/src/fake-clock.c new file mode 100644 index 000000000..a7e8cc82c --- /dev/null +++ b/src/fake-clock.c @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-fake-clock" + +#include "phosh-config.h" + +#include "fake-clock.h" + +#include + +/** + * PhoshWallClockMock: + * + * A wall clock that fakes a constant date and time. The provided date and time + * are determined from `fake-offset`. + */ + +enum { + PROP_0, + PROP_FAKE_OFFSET, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshFakeClock { + PhoshWallClock parent; + + GDateTime *fake_offset; + char *fake_date_time; + char *fake_time; + + GSettings *settings; +}; +G_DEFINE_TYPE (PhoshFakeClock, phosh_fake_clock, PHOSH_TYPE_WALL_CLOCK) + + +static const char * +get_clock (PhoshWallClock *wall_clock, gboolean time_only) +{ + PhoshFakeClock *self = PHOSH_FAKE_CLOCK (wall_clock); + + return time_only ? self->fake_time : self->fake_date_time; +} + + +static gint64 +get_time_t (PhoshWallClock *wall_clock) +{ + PhoshFakeClock *self = PHOSH_FAKE_CLOCK (wall_clock); + + return g_date_time_to_unix (self->fake_offset); +} + + +static void +update_clocks (PhoshFakeClock *self) +{ + GDesktopClockFormat clock_format; + gboolean show_date; + + clock_format = g_settings_get_enum (self->settings, "clock-format"); + show_date = g_settings_get_boolean (self->settings, "clock-show-date"); + + /* Show date and time or only time based on gsettings */ + self->fake_date_time = phosh_wall_clock_string_for_datetime (PHOSH_WALL_CLOCK (self), + self->fake_offset, + clock_format, + show_date); + /* This clock is always time only */ + self->fake_time = phosh_wall_clock_string_for_datetime (PHOSH_WALL_CLOCK (self), + self->fake_offset, + clock_format, + FALSE); +} + + +static void +set_fake_offset (PhoshFakeClock *self, GDateTime *fake_offset) +{ + self->fake_offset = g_date_time_ref (fake_offset); + update_clocks (self); +} + + +static void +on_settings_changed (PhoshFakeClock *self, const char *key, GSettings *settings) +{ + if (g_strcmp0 (key, "clock-format") && g_strcmp0 (key, "clock-show-date") != 0) + return; + + update_clocks (self); +} + + +static void +phosh_fake_clock_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFakeClock *self = PHOSH_FAKE_CLOCK (object); + + switch (property_id) { + case PROP_FAKE_OFFSET: + set_fake_offset (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_fake_clock_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFakeClock *self = PHOSH_FAKE_CLOCK (object); + + switch (property_id) { + case PROP_FAKE_OFFSET: + g_value_set_boxed (value, self->fake_offset); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_fake_clock_dispose (GObject *object) +{ + PhoshFakeClock *self = PHOSH_FAKE_CLOCK (object); + + g_clear_object (&self->settings); + + g_clear_pointer (&self->fake_offset, g_date_time_unref); + g_clear_pointer (&self->fake_date_time, g_free); + g_clear_pointer (&self->fake_time, g_free); + + G_OBJECT_CLASS (phosh_fake_clock_parent_class)->dispose (object); +} + + +static void +phosh_fake_clock_class_init (PhoshFakeClockClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshWallClockClass *wall_clock_class = PHOSH_WALL_CLOCK_CLASS (klass); + + object_class->get_property = phosh_fake_clock_get_property; + object_class->set_property = phosh_fake_clock_set_property; + object_class->dispose = phosh_fake_clock_dispose; + + wall_clock_class->get_clock = get_clock; + wall_clock_class->get_time_t = get_time_t; + + /** + * PhoshFakeClock:fake-offset: + * + * The faked offset from the start of the epoch this clock represents. + */ + props[PROP_FAKE_OFFSET] = + g_param_spec_boxed ("fake-offset", "", "", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_fake_clock_init (PhoshFakeClock *self) +{ + self->settings = g_settings_new ("org.gnome.desktop.interface"); + + g_signal_connect_swapped (self->settings, "changed", G_CALLBACK (on_settings_changed), self); +} + + +PhoshFakeClock * +phosh_fake_clock_new (GDateTime *fake_offset) +{ + return g_object_new (PHOSH_TYPE_FAKE_CLOCK, "fake-offset", fake_offset, NULL); +} diff --git a/src/fake-clock.h b/src/fake-clock.h new file mode 100644 index 000000000..c6add3468 --- /dev/null +++ b/src/fake-clock.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "wall-clock.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FAKE_CLOCK (phosh_fake_clock_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshFakeClock, phosh_fake_clock, PHOSH, FAKE_CLOCK, PhoshWallClock) + +PhoshFakeClock *phosh_fake_clock_new (GDateTime *fake_offset); + +G_END_DECLS diff --git a/src/favorite-list-model.c b/src/favorite-list-model.c new file mode 100644 index 000000000..729ad29ed --- /dev/null +++ b/src/favorite-list-model.c @@ -0,0 +1,314 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "phosh-favorite-list-model" + +#define FAVORITES_KEY "favorites" + +#include "favorite-list-model.h" + +#include "folder-info.h" + +#include + +/** + * PhoshFavoriteListModel: + * + * A `GListModel` of the users favorite applications + * + * Since: 0.1.3 + */ + +typedef struct _PhoshFavoriteListModelPrivate { + /* The complete list as stored in @settings */ + GStrv items_inc_missing; + + /* The sanitised list */ + GStrv items; + /* Cached length of @items */ + guint len; + + GSettings *settings; +} PhoshFavoriteListModelPrivate; + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshFavoriteListModel, phosh_favorite_list_model, G_TYPE_OBJECT, + G_ADD_PRIVATE (PhoshFavoriteListModel) + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +static void +phosh_favorite_list_model_finalize (GObject *object) +{ + PhoshFavoriteListModel *self = PHOSH_FAVORITE_LIST_MODEL (object); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (self); + + g_clear_object (&priv->settings); + + g_clear_pointer (&priv->items_inc_missing, g_strfreev); + g_clear_pointer (&priv->items, g_strfreev); + + G_OBJECT_CLASS (phosh_favorite_list_model_parent_class)->finalize (object); +} + + +static void +phosh_favorite_list_model_class_init (PhoshFavoriteListModelClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = phosh_favorite_list_model_finalize; +} + + +static GType +list_get_item_type (GListModel *list) +{ + return G_TYPE_APP_INFO; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshFavoriteListModel *self = PHOSH_FAVORITE_LIST_MODEL (list); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (self); + + if (position >= priv->len) { + return NULL; + } + + return g_desktop_app_info_new (priv->items[position]); +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshFavoriteListModel *self = PHOSH_FAVORITE_LIST_MODEL (list); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (self); + + return priv->len; +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static void +favorites_changed (GSettings *settings, + const char *key, + PhoshFavoriteListModel *self) +{ + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (self); + int removed; + int added = 0; + int new_length = 0; + int i = 0; + + /* Clear the old items */ + removed = priv->len; + + g_clear_pointer (&priv->items_inc_missing, g_strfreev); + g_clear_pointer (&priv->items, g_strfreev); + + /* Get the new list */ + priv->items_inc_missing = g_settings_get_strv (settings, key); + new_length = g_strv_length (priv->items_inc_missing); + + priv->items = g_new (char *, new_length + 1); + + while (priv->items_inc_missing[i]) { + g_autoptr (GDesktopAppInfo) info = NULL; + + /* We don't actually care about this value, just that it isn't NULL */ + info = g_desktop_app_info_new (priv->items_inc_missing[i]); + + if (G_LIKELY (info != NULL)) { + priv->items[added] = g_strdup (priv->items_inc_missing[i]); + added++; + } else { + g_debug ("Missing favorite %s, skipping", priv->items_inc_missing[i]); + } + + i++; + } + priv->items[added] = NULL; + + priv->len = added; + + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); +} + + +static void +phosh_favorite_list_model_init (PhoshFavoriteListModel *self) +{ + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (self); + + priv->items_inc_missing = NULL; + priv->items = NULL; + priv->len = 0; + + priv->settings = g_settings_new ("sm.puri.phosh"); + g_signal_connect (priv->settings, "changed::" FAVORITES_KEY, + G_CALLBACK (favorites_changed), self); + favorites_changed (priv->settings, FAVORITES_KEY, self); +} + + +/** + * phosh_favorite_list_model_get_default: + * + * Returns: (transfer none): The global #PhoshFavoriteListModel singleton + */ +PhoshFavoriteListModel * +phosh_favorite_list_model_get_default (void) +{ + static PhoshFavoriteListModel *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_FAVORITE_LIST_MODEL, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *) &instance); + } + + return instance; +} + + +/** + * phosh_favorite_list_model_app_is_favorite: + * @self: (nullable): the #PhoshFavoriteListModel, use %NULL for the default + * @app: a #GAppInfo to lookup + * + * Returns: %TRUE if @app if currently favorited, otherwise %FALSE + */ +gboolean +phosh_favorite_list_model_app_is_favorite (PhoshFavoriteListModel *self, + GAppInfo *app) +{ + PhoshFavoriteListModel *list = self != NULL ? self : phosh_favorite_list_model_get_default (); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (list); + const char *id; + + if (PHOSH_IS_FOLDER_INFO (app)) + return FALSE; + + g_return_val_if_fail (G_IS_APP_INFO (app), FALSE); + + id = g_app_info_get_id (app); + + if (G_UNLIKELY (id == NULL)) { + return FALSE; + } + + if (g_strv_contains ((const char *const *) priv->items_inc_missing, id)) { + return TRUE; + } + + return FALSE; +} + + +void +phosh_favorite_list_model_add_app (PhoshFavoriteListModel *self, + GAppInfo *app) +{ + PhoshFavoriteListModel *list = self != NULL ? self : phosh_favorite_list_model_get_default (); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (list); + const char *id; + int old_length = 0; + g_auto (GStrv) new_favorites = NULL; + + g_return_if_fail (G_IS_APP_INFO (app)); + + id = g_app_info_get_id (app); + + if (G_UNLIKELY (id == NULL)) { + g_critical ("Can't add `%p`, doesn't have an id", app); + + return; + } + + old_length = g_strv_length (priv->items_inc_missing); + + new_favorites = g_new0 (char *, old_length + 2); + + for (int i = 0; i < old_length; i++) { + /* Avoid having the same favorite twice */ + if (G_UNLIKELY (g_strcmp0 (priv->items_inc_missing[i], id) == 0)) { + g_warning ("%s is already a favorite", id); + + return; + } + new_favorites[i] = g_strdup (priv->items_inc_missing[i]); + } + /* Add the new id */ + new_favorites[old_length] = g_strdup (id); + new_favorites[old_length + 1] = NULL; + + /* Indirectly calls favorites_changed which updates the model */ + g_settings_set_strv (priv->settings, + FAVORITES_KEY, + (const char *const *) new_favorites); +} + + +void +phosh_favorite_list_model_remove_app (PhoshFavoriteListModel *self, + GAppInfo *app) +{ + PhoshFavoriteListModel *list = self != NULL ? self : phosh_favorite_list_model_get_default (); + PhoshFavoriteListModelPrivate *priv = phosh_favorite_list_model_get_instance_private (list); + const char *id; + int old_length = 0; + int new_idx = 0; + int old_idx = 0; + g_auto(GStrv) new_favorites = NULL; + + g_return_if_fail (G_IS_APP_INFO (app)); + + id = g_app_info_get_id (app); + + if (G_UNLIKELY (id == NULL)) { + g_critical ("Can't remove `%p`, doesn't have an id", app); + + return; + } + + old_length = g_strv_length (priv->items_inc_missing); + + new_favorites = g_new (char *, old_length + 1); + + while (priv->items_inc_missing[old_idx]) { + /* Skip over the favorite being removed */ + if (G_LIKELY (g_strcmp0 (priv->items_inc_missing[old_idx], id) != 0)) { + new_favorites[new_idx] = g_strdup (priv->items_inc_missing[old_idx]); + new_idx++; + } + old_idx++; + } + new_favorites[new_idx] = NULL; + + /* If we actually removed id then old_idx should be ahead of new_idx */ + if (G_UNLIKELY (old_idx <= new_idx)) { + g_warning ("%s wasn't a favorite", id); + + return; + } + + /* Indirectly calls favorites_changed which updates the model */ + g_settings_set_strv (priv->settings, + FAVORITES_KEY, + (const char *const *) new_favorites); +} diff --git a/src/favorite-list-model.h b/src/favorite-list-model.h new file mode 100644 index 000000000..9ad3e94b0 --- /dev/null +++ b/src/favorite-list-model.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FAVORITE_LIST_MODEL phosh_favorite_list_model_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshFavoriteListModel, phosh_favorite_list_model, PHOSH, FAVORITE_LIST_MODEL, GObject) + +struct _PhoshFavoriteListModelClass +{ + GObjectClass parent_class; +}; + +PhoshFavoriteListModel *phosh_favorite_list_model_get_default (void); +gboolean phosh_favorite_list_model_app_is_favorite (PhoshFavoriteListModel *self, + GAppInfo *app); +void phosh_favorite_list_model_add_app (PhoshFavoriteListModel *self, + GAppInfo *app); +void phosh_favorite_list_model_remove_app (PhoshFavoriteListModel *self, + GAppInfo *app); + +G_END_DECLS diff --git a/src/feedback-manager.c b/src/feedback-manager.c new file mode 100644 index 000000000..5b88f4792 --- /dev/null +++ b/src/feedback-manager.c @@ -0,0 +1,359 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-feedback-manager" + +#include "phosh-config.h" + +#include "feedback-manager.h" +#include "shell-priv.h" +#include "util.h" + +#include + +/** + * PhoshFeedbackManager: + * + * Sends and configures user feedback + */ + +#define PHOSH_FEEDBACK_ICON_FULL "preferences-system-notifications-symbolic" +#define PHOSH_FEEDBACK_ICON_QUIET "feedback-quiet-symbolic" +#define PHOSH_FEEDBACK_ICON_SILENT "notifications-disabled-symbolic" + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_PROFILE, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshFeedbackManager { + GObject parent; + + const char *profile; + const char *icon_name; + gboolean inited; + + /* signal emission hooks */ + gulong button_clicked_hook_id; + gulong row_activated_hook_id; + gulong long_press_hook_id; +}; + +G_DEFINE_TYPE (PhoshFeedbackManager, phosh_feedback_manager, G_TYPE_OBJECT); + + +static void +on_event_triggered (LfbEvent *event, + GAsyncResult *res, + LfbEvent **cmp) +{ + g_autoptr (GError) err = NULL; + + if (!lfb_event_trigger_feedback_finish (event, res, &err)) { + if (g_error_matches (err, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) { + g_debug ("Feedbackd service not found: %s", err->message); + } else { + g_warning ("Failed to trigger feedback for '%s': %s", + lfb_event_get_event (event), err->message); + } + } +} + + +static void +phosh_feedback_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFeedbackManager *self = PHOSH_FEEDBACK_MANAGER (object); + + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_PROFILE: + g_value_set_string (value, self->profile); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->inited); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_feedback_manager_update (PhoshFeedbackManager *self) +{ + const char *icon_name = self->icon_name; + const char *profile = self->profile; + + self->profile = lfb_get_feedback_profile (); + if (g_strcmp0 (self->profile, "quiet") == 0) + self->icon_name = PHOSH_FEEDBACK_ICON_QUIET; + else if (g_strcmp0 (self->profile, "silent") == 0) + self->icon_name = PHOSH_FEEDBACK_ICON_SILENT; + else + self->icon_name = PHOSH_FEEDBACK_ICON_FULL; + + g_debug("Feedback profile set to: '%s', icon '%s'", self->profile, self->icon_name); + + if (profile != self->profile) + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PROFILE]); + if (icon_name != self->icon_name) + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +static void +on_profile_changed (PhoshFeedbackManager *self, GParamSpec *psepc, LfbGdbusFeedback *proxy) +{ + g_return_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self)); + + phosh_feedback_manager_update (self); +} + + +static gboolean +on_signal_hook (GSignalInvocationHint *ihint, + guint n_param_values, + const GValue *param_values, + gpointer user_data) +{ + const char *event_name = user_data; + + g_return_val_if_fail (event_name, TRUE); + + phosh_trigger_feedback (event_name); + return TRUE; +} + + +static void +phosh_feedback_manager_constructed (GObject *object) +{ + PhoshFeedbackManager *self = PHOSH_FEEDBACK_MANAGER (object); + g_autoptr(GError) error = NULL; + + G_OBJECT_CLASS (phosh_feedback_manager_parent_class)->constructed (object); + + if (lfb_init (PHOSH_APP_ID, &error)) { + g_debug ("Libfeedback inited"); + self->inited = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PROFILE]); + } else { + g_warning ("Failed to init libfeedback: %s", error->message); + return; + } + + g_signal_connect_swapped (lfb_get_proxy (), + "notify::profile", + (GCallback)on_profile_changed, + self); + phosh_feedback_manager_update (self); +} + + +static gulong +connect_hook (const char *signal_name, GType type, const char *event_name) +{ + g_autoptr (GTypeClass) type_class = NULL; + guint signal_id; + gulong hook_id; + + type_class = g_type_class_ref (type); + signal_id = g_signal_lookup (signal_name, type); + hook_id = g_signal_add_emission_hook (signal_id, 0, on_signal_hook, (gpointer)event_name, NULL); + + return hook_id; +} + + +static void +clear_hook (gulong *hook_id, const char *signal_name, GType type) +{ + g_autoptr (GTypeClass) type_class = NULL; + guint signal_id; + + if (!*hook_id) + return; + + type_class = g_type_class_ref (type); + signal_id = g_signal_lookup (signal_name, type); + g_signal_remove_emission_hook (signal_id, *hook_id); + + *hook_id = 0; +} + + +static void +phosh_feedback_manager_finalize (GObject *object) +{ + PhoshFeedbackManager *self = PHOSH_FEEDBACK_MANAGER (object); + + if (self->inited) { + g_signal_handlers_disconnect_by_data (lfb_get_proxy (), self); + lfb_uninit (); + self->inited = FALSE; + } + + clear_hook (&self->button_clicked_hook_id, "clicked", GTK_TYPE_BUTTON); + clear_hook (&self->row_activated_hook_id, "row-activated", GTK_TYPE_LIST_BOX); + clear_hook (&self->long_press_hook_id, "pressed", GTK_TYPE_GESTURE_LONG_PRESS); + + G_OBJECT_CLASS (phosh_feedback_manager_parent_class)->finalize (object); +} + + +static void +phosh_feedback_manager_class_init (PhoshFeedbackManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_feedback_manager_constructed; + object_class->finalize = phosh_feedback_manager_finalize; + + object_class->get_property = phosh_feedback_manager_get_property; + + /** + * PhoshFeedbackManager:icon-name: + * + * The feedback icon name + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + PHOSH_FEEDBACK_ICON_FULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshFeedbackManager:profile: + * + * The feedback profile name + */ + props[PROP_PROFILE] = + g_param_spec_string ("profile", "", "", + "", + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshFeedbackManager:present: + * + * Whether feedback manager is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_feedback_manager_init (PhoshFeedbackManager *self) +{ + self->icon_name = PHOSH_FEEDBACK_ICON_FULL; +} + + +PhoshFeedbackManager * +phosh_feedback_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_FEEDBACK_MANAGER, NULL); +} + + +const char * +phosh_feedback_manager_get_icon_name (PhoshFeedbackManager *self) +{ + g_return_val_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self), NULL); + + return self->icon_name; +} + + +const char * +phosh_feedback_manager_get_profile (PhoshFeedbackManager *self) +{ + g_return_val_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self), NULL); + + return self->profile; +} + + +void +phosh_feedback_manager_set_profile (PhoshFeedbackManager *self, const char *profile) +{ + g_return_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self)); + + g_debug ("Setting feedback profile to %s", profile); + lfb_set_feedback_profile (profile); +} + + +void +phosh_feedback_manager_toggle (PhoshFeedbackManager *self) +{ + const char *profile = "silent", *old; + + g_return_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self)); + g_return_if_fail (self->inited); + + old = lfb_get_feedback_profile (); + if (!g_strcmp0 (old, "silent")) + profile = "full"; + else if (!g_strcmp0 (old, "full")) + profile = "quiet"; + + phosh_feedback_manager_set_profile (self, profile); +} + + +/** + * phosh_trigger_feedback: + * @name: The event's name to trigger feedback for + * + * Trigger feedback for the given event asynchronously + */ +void +phosh_trigger_feedback (const char *name) +{ + g_autoptr (LfbEvent) event = NULL; + + g_return_if_fail (lfb_is_initted ()); + g_return_if_fail (name); + + event = lfb_event_new (name); + lfb_event_trigger_feedback_async (event, + NULL, + (GAsyncReadyCallback)on_event_triggered, + NULL); +} + +/** + * phosh_feedback_manager_setup_hooks: + * @self: The feedback manager + * + * Setup signal emission hooks that trigger event feedback. + */ +void +phosh_feedback_manager_setup_event_hooks (PhoshFeedbackManager *self) +{ + g_return_if_fail (PHOSH_IS_FEEDBACK_MANAGER (self)); + + self->button_clicked_hook_id = connect_hook ("clicked", GTK_TYPE_BUTTON, "button-pressed"); + self->row_activated_hook_id = connect_hook ("row-activated", GTK_TYPE_LIST_BOX, "button-pressed"); + self->long_press_hook_id = connect_hook ("pressed", + GTK_TYPE_GESTURE_LONG_PRESS, + "button-pressed"); +} diff --git a/src/feedback-manager.h b/src/feedback-manager.h new file mode 100644 index 000000000..3b3950b43 --- /dev/null +++ b/src/feedback-manager.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include + +#define PHOSH_TYPE_FEEDBACK_MANAGER (phosh_feedback_manager_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshFeedbackManager, + phosh_feedback_manager, + PHOSH, + FEEDBACK_MANAGER, + GObject) + +PhoshFeedbackManager *phosh_feedback_manager_new (void); +void phosh_feedback_manager_toggle (PhoshFeedbackManager *self); +const char * phosh_feedback_manager_get_icon_name (PhoshFeedbackManager *self); +const char * phosh_feedback_manager_get_profile (PhoshFeedbackManager *self); +void phosh_feedback_manager_set_profile (PhoshFeedbackManager *self, const char *profile); +void phosh_feedback_manager_trigger_feedback (PhoshFeedbackManager *self, const char *event); +void phosh_trigger_feedback (const char *name); +void phosh_feedback_manager_setup_event_hooks (PhoshFeedbackManager *self); diff --git a/src/feedback-status-page.c b/src/feedback-status-page.c new file mode 100644 index 000000000..321f52335 --- /dev/null +++ b/src/feedback-status-page.c @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-phosh-feedback-status-page" + +#include "phosh-config.h" + +#include "feedback-status-page.h" +#include "feedback-manager.h" +#include "notifications/notify-manager.h" +#include "shell-priv.h" + +/** + * Phosh_feedback_status_page: + * + * The status page for the feedback settings + */ + +enum { + PROP_0, + PROP_DO_NOT_DISTURB, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshFeedbackStatusPage { + PhoshStatusPage parent; + + GtkSwitch *dnd_switch; + GtkImage *icon; + gboolean do_not_disturb; + char *prev_profile; +}; +G_DEFINE_TYPE (PhoshFeedbackStatusPage, phosh_feedback_status_page, PHOSH_TYPE_STATUS_PAGE) + +static gboolean +dnd_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean do_not_disturb = g_value_get_boolean (from_value); + const char *icon_name; + + icon_name = do_not_disturb ? "chat-none-symbolic" : "chat-symbolic"; + g_value_set_string (to_value, icon_name); + return TRUE; +} + + +static void +set_do_not_disturb (PhoshFeedbackStatusPage *self, gboolean do_not_disturb) +{ + PhoshFeedbackManager *manager = phosh_shell_get_feedback_manager (phosh_shell_get_default ()); + + if (self->do_not_disturb == do_not_disturb) + return; + + g_debug ("Do not disturb: %d", do_not_disturb); + self->do_not_disturb = do_not_disturb; + + if (self->do_not_disturb) { + g_free (self->prev_profile); + self->prev_profile = g_strdup (phosh_feedback_manager_get_profile (manager)); + phosh_feedback_manager_set_profile (manager, "silent"); + } else { + /* Restore previous profile only if user didn't change it in between */ + if (g_str_equal (phosh_feedback_manager_get_profile (manager), "silent") && + self->prev_profile) + phosh_feedback_manager_set_profile (manager, self->prev_profile); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DO_NOT_DISTURB]); + g_signal_emit_by_name (self, "done", NULL); +} + + +static void +on_dnd_row_activated (PhoshFeedbackStatusPage *self) +{ + g_assert (PHOSH_IS_FEEDBACK_STATUS_PAGE (self)); + set_do_not_disturb (self, !self->do_not_disturb); +} + + +static void +phosh_feedback_status_page_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFeedbackStatusPage *self = PHOSH_FEEDBACK_STATUS_PAGE (object); + + switch (property_id) { + case PROP_DO_NOT_DISTURB: + set_do_not_disturb (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_feedback_status_page_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFeedbackStatusPage *self = PHOSH_FEEDBACK_STATUS_PAGE (object); + + switch (property_id) { + case PROP_DO_NOT_DISTURB: + g_value_set_boolean (value, self->do_not_disturb); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_feedback_status_page_finalize (GObject *object) +{ + PhoshFeedbackStatusPage *self = PHOSH_FEEDBACK_STATUS_PAGE (object); + + g_clear_pointer (&self->prev_profile, g_free); + + G_OBJECT_CLASS (phosh_feedback_status_page_parent_class)->finalize (object); +} + + +static void +phosh_feedback_status_page_class_init (PhoshFeedbackStatusPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_feedback_status_page_finalize; + object_class->get_property = phosh_feedback_status_page_get_property; + object_class->set_property = phosh_feedback_status_page_set_property; + + + props[PROP_DO_NOT_DISTURB] = + g_param_spec_boolean ("do-not-disturb", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/feedback-status-page.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshFeedbackStatusPage, dnd_switch); + gtk_widget_class_bind_template_child (widget_class, PhoshFeedbackStatusPage, icon); + + gtk_widget_class_bind_template_callback (widget_class, on_dnd_row_activated); +} + + +static void +phosh_feedback_status_page_init (PhoshFeedbackStatusPage *self) +{ + g_autoptr (GSettings) settings = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property (self, + "do-not-disturb", + self->dnd_switch, + "active", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self, + "do-not-disturb", + self->icon, + "icon-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + dnd_to_icon_name, + NULL, NULL, NULL); + + settings = g_settings_new (PHOSH_NOTIFICATIONS_SCHEMA_ID); + g_settings_bind (settings, + PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS, + self, + "do-not-disturb", + G_SETTINGS_BIND_DEFAULT | G_SETTINGS_BIND_INVERT_BOOLEAN); +} + + +PhoshFeedbackStatusPage * +phosh_feedback_status_page_new (void) +{ + return g_object_new (PHOSH_TYPE_FEEDBACK_STATUS_PAGE, NULL); +} diff --git a/src/feedback-status-page.h b/src/feedback-status-page.h new file mode 100644 index 000000000..7d606a504 --- /dev/null +++ b/src/feedback-status-page.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "status-page.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FEEDBACK_STATUS_PAGE (phosh_feedback_status_page_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshFeedbackStatusPage, phosh_feedback_status_page, PHOSH, FEEDBACK_STATUS_PAGE, + PhoshStatusPage) + +PhoshFeedbackStatusPage *phosh_feedback_status_page_new (void); + +G_END_DECLS diff --git a/src/feedbackinfo.c b/src/feedbackinfo.c new file mode 100644 index 000000000..17d75bcf9 --- /dev/null +++ b/src/feedbackinfo.c @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-feedbackinfo" + +#include "phosh-config.h" + +#include "feedbackinfo.h" +#include "shell-priv.h" + +/** + * PhoshFeedbackInfo: + * + * A widget to display feedback status + */ +enum { + PROP_0, + PROP_MUTED, + PROP_PRESENT, + PROP_ENABLED, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshFeedbackInfo { + PhoshStatusIcon parent; + + PhoshFeedbackManager *manager; + gboolean muted; + gboolean present; + gboolean enabled; +} PhoshFeedbackInfo; + + +G_DEFINE_TYPE (PhoshFeedbackInfo, phosh_feedback_info, PHOSH_TYPE_STATUS_ICON) + +static void +phosh_feedback_info_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFeedbackInfo *self = PHOSH_FEEDBACK_INFO (object); + + switch (prop_id) { + case PROP_PRESENT: + self->present = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +phosh_feedback_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFeedbackInfo *self = PHOSH_FEEDBACK_INFO (object); + + switch (property_id) { + case PROP_MUTED: + g_value_set_boolean (value, self->muted); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +on_profile_changed (PhoshFeedbackInfo *self, GParamSpec *psepc, gpointer unused) +{ + const char *profile, *name; + gboolean muted = FALSE; + gboolean enabled = FALSE; + + g_return_if_fail (PHOSH_IS_FEEDBACK_INFO (self)); + + profile = phosh_feedback_manager_get_profile (self->manager); + if (!g_strcmp0 (profile, "quiet")) { + /* Translators: quiet and silent are fbd profiles names: + see https://source.puri.sm/Librem5/feedbackd#profiles + for details */ + name = _("Quiet"); + muted = TRUE; + enabled = TRUE; + } else if (!g_strcmp0 (profile, "silent")) { + /* Translators: quiet and silent are fbd profiles names: + see https://source.puri.sm/Librem5/feedbackd#profiles + for details */ + name = _("Silent"); + muted = TRUE; + } else { + /* Translators: Enable LED, haptic and audio feedback */ + name = C_("feedback:enabled", "On"); + enabled = TRUE; + } + + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), name); + + if (muted != self->muted) { + self->muted = muted; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MUTED]); + } + + if (enabled != self->enabled) { + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); + } +} + + +static void +phosh_feedback_info_constructed (GObject *object) +{ + PhoshFeedbackInfo *self = PHOSH_FEEDBACK_INFO(object); + PhoshShell *shell = phosh_shell_get_default (); + + G_OBJECT_CLASS (phosh_feedback_info_parent_class)->constructed (object); + + self->manager = g_object_ref(phosh_shell_get_feedback_manager (shell)); + + g_signal_connect_swapped (self->manager, + "notify::profile", + G_CALLBACK(on_profile_changed), + self); + on_profile_changed (self, NULL, NULL); + g_object_bind_property (self->manager, "icon-name", self, "icon-name", + G_BINDING_SYNC_CREATE); + g_object_bind_property (self->manager, "present", self, "present", + G_BINDING_SYNC_CREATE); +} + + +static void +phosh_feedback_info_dispose (GObject *object) +{ + PhoshFeedbackInfo *self = PHOSH_FEEDBACK_INFO(object); + + if (self->manager) { + g_signal_handlers_disconnect_by_data (self->manager, self); + g_clear_object (&self->manager); + } + + G_OBJECT_CLASS (phosh_feedback_info_parent_class)->dispose (object); +} + + +static void +phosh_feedback_info_class_init (PhoshFeedbackInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_feedback_info_constructed; + object_class->dispose = phosh_feedback_info_dispose; + object_class->set_property = phosh_feedback_info_set_property; + object_class->get_property = phosh_feedback_info_get_property; + + /** + * PhoshFeedbackInfo:muted: + * + * Whether audio is muted (this is true for the `quiet` and `silent` + * profiles) but not for the `full` profile. + */ + props[PROP_MUTED] = + g_param_spec_boolean ("muted", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshFeedbackinfo:present: + * + * Whether feedback manager is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshFeedbackInfo:enabled: + * + * Whether feedback is enabled. This is true for the `full` and `quiet` profile. + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "phosh-feedback-info"); +} + + +static void +phosh_feedback_info_init (PhoshFeedbackInfo *self) +{ +} + + +GtkWidget * +phosh_feedback_info_new (void) +{ + return g_object_new (PHOSH_TYPE_FEEDBACK_INFO, NULL); +} diff --git a/src/feedbackinfo.h b/src/feedbackinfo.h new file mode 100644 index 000000000..1f74fe3bc --- /dev/null +++ b/src/feedbackinfo.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_FEEDBACK_INFO (phosh_feedback_info_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshFeedbackInfo, phosh_feedback_info, PHOSH, FEEDBACK_INFO, PhoshStatusIcon) + +GtkWidget * phosh_feedback_info_new (void); + +G_END_DECLS diff --git a/src/folder-info.c b/src/folder-info.c new file mode 100644 index 000000000..2e2d8a5e6 --- /dev/null +++ b/src/folder-info.c @@ -0,0 +1,478 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-folder-info" + +#include "folder-info.h" +#include "util.h" + +#include "favorite-list-model.h" +#include "gtk-list-models/gtkfilterlistmodel.h" + +#include + +#include + +#define FOLDER_SCHEMA_ID "org.gnome.desktop.app-folders.folder" +#define FOLDERS_PREFIX "/org/gnome/desktop/app-folders/folders" + +/** + * PhoshFolderInfo: + * + * An object that represents a list of applications belonging to a folder. + */ + +enum { + PROP_0, + PROP_PATH, + PROP_NAME, + PROP_APP_INFOS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + APPS_CHANGED, + N_SIGNALS, +}; +static guint signals[N_SIGNALS]; + +struct _PhoshFolderInfo { + GObject parent; + + char *path; + char *name; + + /* Contains all apps belonging to the folder */ + GListStore *app_infos; + /* Filters the above to show only required apps, + * like non-favorite etc. */ + GtkFilterListModel *filtered_app_infos; + + PhoshFavoriteListModel *favorites; + GSettings *settings; + + /* The current search term (only valid during refilter) */ + const char *search; +}; + +static void folder_info_iface_init (GAppInfoIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshFolderInfo, phosh_folder_info, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_APP_INFO, folder_info_iface_init)) + + +static void +phosh_folder_info_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (object); + + switch (property_id) { + case PROP_PATH: + self->path = g_value_dup_string (value); + break; + case PROP_NAME: + phosh_folder_info_set_name (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_folder_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (object); + + switch (property_id) { + case PROP_PATH: + g_value_set_string (value, self->path); + break; + case PROP_NAME: + g_value_set_string (value, self->name); + break; + case PROP_APP_INFOS: + g_value_set_object (value, self->filtered_app_infos); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static gboolean +equal (GAppInfo *app_info1, GAppInfo *app_info2) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (app_info1); + PhoshFolderInfo *other; + gboolean is_equal; + + if (!PHOSH_IS_FOLDER_INFO (app_info2)) + return FALSE; + + other = PHOSH_FOLDER_INFO (app_info2); + is_equal = g_str_equal (self->path, other->path); + + return is_equal; +} + + +static const char * +get_name (GAppInfo *app_info) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (app_info); + + return self->name; +} + + +static gboolean +should_show (GAppInfo *app_info) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (app_info); + g_autoptr (GAppInfo) item = NULL; + + item = g_list_model_get_item (G_LIST_MODEL (self->filtered_app_infos), 0); + return item != NULL; +} + + +static char * +load_from_file (char *filename, const char *data_dir) +{ + g_autofree char *path = g_build_path ("/", data_dir, "desktop-directories", filename, NULL); + g_autoptr (GKeyFile) keyfile = g_key_file_new (); + g_autoptr (GError) error = NULL; + char *dirname; + + g_key_file_load_from_file (keyfile, path, G_KEY_FILE_NONE, &error); + if (error != NULL) { + g_debug ("Unable to load desktop directory %s: %s", path, error->message); + return NULL; + } + + dirname = g_key_file_get_locale_string (keyfile, "Desktop Entry", "Name", NULL, &error); + if (!dirname) { + g_debug ("Unable to load name from %s: %s", path, error->message); + return NULL; + } + + return dirname; +} + + +static void +on_settings_name_changed (PhoshFolderInfo *self, GSettings *settings, char *key) +{ + g_autofree char *name = g_settings_get_string (self->settings, "name"); + + /* Load name from .directory file if available */ + if (g_str_has_suffix (name, ".directory")) { + const char *const *data_dirs = g_get_system_data_dirs (); + for (int i = 0; data_dirs[i] != NULL; i++) { + g_autofree char *dirname = load_from_file (name, data_dirs[i]); + if (dirname) { + g_free (name); + name = g_steal_pointer (&dirname); + break; + } + } + } + + if (g_strcmp0 (name, self->name) == 0) + return; + + g_clear_pointer (&self->name, g_free); + self->name = g_steal_pointer (&name); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]); +} + + +static void +load_apps (PhoshFolderInfo *self) +{ + g_auto (GStrv) apps; + + apps = g_settings_get_strv (self->settings, "apps"); + + for (int i = 0; apps[i]; i++) { + g_autoptr (GDesktopAppInfo) app_info = g_desktop_app_info_new (apps[i]); + + if (app_info == NULL) + g_debug ("Unable to load app-info for %s", apps[i]); + else if (!g_app_info_should_show (G_APP_INFO (app_info))) + continue; + else + g_list_store_append (self->app_infos, app_info); + } +} + + +static void +on_settings_apps_changed (PhoshFolderInfo *self, GSettings *settings, char *key) +{ + g_signal_emit (self, signals[APPS_CHANGED], 0); + g_list_store_remove_all (self->app_infos); + load_apps (self); +} + + +static gboolean +filter_app (gpointer item, gpointer data) +{ + GAppInfo *app_info = G_APP_INFO (item); + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (data); + gboolean show; + + /* + * If nothing is being searched, then show the app only if it is non-favorite. + * Else check if the app matches the search term. + */ + if (gm_str_is_null_or_empty (self->search)) + show = !phosh_favorite_list_model_app_is_favorite (self->favorites, app_info); + else + show = phosh_util_matches_app_info (app_info, self->search); + + return show; +} + + +static void +phosh_folder_info_dispose (GObject *object) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (object); + + g_clear_pointer (&self->path, g_free); + g_clear_pointer (&self->name, g_free); + g_clear_object (&self->filtered_app_infos); + g_clear_object (&self->app_infos); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_folder_info_parent_class)->dispose (object); +} + + +static void +phosh_folder_info_constructed (GObject *object) +{ + PhoshFolderInfo *self = PHOSH_FOLDER_INFO (object); + g_autofree char *path = NULL; + + G_OBJECT_CLASS (phosh_folder_info_parent_class)->constructed (object); + + self->app_infos = g_list_store_new (G_TYPE_APP_INFO); + self->favorites = phosh_favorite_list_model_get_default (); + self->filtered_app_infos = gtk_filter_list_model_new (G_LIST_MODEL (self->app_infos), + filter_app, + self, + NULL); + path = g_strconcat (FOLDERS_PREFIX, "/", self->path, "/", NULL); + self->settings = g_settings_new_with_path (FOLDER_SCHEMA_ID, path); + + g_signal_connect_object (self->settings, "changed::name", + G_CALLBACK (on_settings_name_changed), self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->settings, "changed::apps", + G_CALLBACK (on_settings_apps_changed), self, G_CONNECT_SWAPPED); + + on_settings_name_changed (self, NULL, NULL); + load_apps (self); + gtk_filter_list_model_refilter (self->filtered_app_infos); +} + +static void +folder_info_iface_init (GAppInfoIface *iface) +{ + iface->equal = equal; + iface->get_name = get_name; + iface->should_show = should_show; +} + + +static void +phosh_folder_info_class_init (PhoshFolderInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_folder_info_dispose; + object_class->constructed = phosh_folder_info_constructed; + object_class->set_property = phosh_folder_info_set_property; + object_class->get_property = phosh_folder_info_get_property; + + /** + * PhoshFolderInfo:path: + * + * Relative GSettings path to folder + */ + props[PROP_PATH] = + g_param_spec_string ("path", "", "", + NULL, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + /** + * PhoshFolderInfo:name: + * + * Name of the folder + */ + props[PROP_NAME] = + g_param_spec_string ("name", "", "", + NULL, + G_PARAM_READWRITE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + /** + * PhoshFolderInfo:app-infos: + * + * A list model of (filtered) app-infos belonging to the folder. + */ + props[PROP_APP_INFOS] = + g_param_spec_object ("app-infos", "", "", + G_TYPE_LIST_MODEL, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[APPS_CHANGED] = g_signal_new ("apps-changed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + + +static void +phosh_folder_info_init (PhoshFolderInfo *self) +{ +} + + +PhoshFolderInfo * +phosh_folder_info_new_from_folder_path (char *path) +{ + return g_object_new (PHOSH_TYPE_FOLDER_INFO, "path", path, NULL); +} + + +char * +phosh_folder_info_get_name (PhoshFolderInfo *self) +{ + g_return_val_if_fail (PHOSH_IS_FOLDER_INFO (self), 0); + + return self->name; +} + +void +phosh_folder_info_set_name (PhoshFolderInfo *self, const char *name) +{ + g_return_if_fail (PHOSH_IS_FOLDER_INFO (self)); + + g_settings_set_string (self->settings, "name", name); +} + +/** + * phosh_folder_info_get_app_infos: + * @self: A folder info + * + * Get the list model of the folder info. + * + * Returns:(transfer none): The folder info + */ +GListModel * +phosh_folder_info_get_app_infos (PhoshFolderInfo *self) +{ + g_return_val_if_fail (PHOSH_IS_FOLDER_INFO (self), 0); + + return G_LIST_MODEL (self->filtered_app_infos); +} + + +gboolean +phosh_folder_info_contains (PhoshFolderInfo *self, GAppInfo *app_info) +{ + gboolean found = FALSE; + g_return_val_if_fail (PHOSH_IS_FOLDER_INFO (self), FALSE); + + found = g_list_store_find_with_equal_func (self->app_infos, app_info, + (GEqualFunc) g_app_info_equal, NULL); + + return found; +} + + +gboolean +phosh_folder_info_refilter (PhoshFolderInfo *self, const char *search) +{ + g_autoptr (GAppInfo) item = NULL; + g_return_val_if_fail (PHOSH_IS_FOLDER_INFO (self), FALSE); + + self->search = search; + gtk_filter_list_model_refilter (self->filtered_app_infos); + self->search = NULL; + + item = g_list_model_get_item (G_LIST_MODEL (self->filtered_app_infos), 0); + return item != NULL; +} + + +void +phosh_folder_info_add_app_info (PhoshFolderInfo *self, GAppInfo *app_info) +{ + const char *app_id; + g_auto (GStrv) apps = NULL; + g_auto (GStrv) new_apps = NULL; + + g_return_if_fail (PHOSH_IS_FOLDER_INFO (self)); + + app_id = g_app_info_get_id (app_info); + + if (app_id == NULL) { + g_debug ("Unable to get application ID"); + return; + } + + apps = g_settings_get_strv (self->settings, "apps"); + new_apps = phosh_util_append_to_strv (apps, app_id); + + g_settings_set_strv (self->settings, "apps", (const char *const *) new_apps); +} + + +gboolean +phosh_folder_info_remove_app_info (PhoshFolderInfo *self, GAppInfo *app_info) +{ + const char *app_id; + g_auto (GStrv) apps = NULL; + g_auto (GStrv) new_apps = NULL; + + g_return_val_if_fail (PHOSH_IS_FOLDER_INFO (self), FALSE); + + app_id = g_app_info_get_id (app_info); + + if (app_id == NULL) { + g_debug ("Unable to get application ID"); + return FALSE; + } + + apps = g_settings_get_strv (self->settings, "apps"); + new_apps = phosh_util_remove_from_strv (apps, app_id); + + g_settings_set_strv (self->settings, "apps", (const char *const *) new_apps); + return new_apps[0] != NULL; +} diff --git a/src/folder-info.h b/src/folder-info.h new file mode 100644 index 000000000..3aa2e3e0f --- /dev/null +++ b/src/folder-info.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_FOLDERS_SCHEMA_ID "org.gnome.desktop.app-folders" + +#define PHOSH_TYPE_FOLDER_INFO phosh_folder_info_get_type () +G_DECLARE_FINAL_TYPE (PhoshFolderInfo, phosh_folder_info, PHOSH, FOLDER_INFO, GObject) + +PhoshFolderInfo *phosh_folder_info_new_from_folder_path (char *path); + +char *phosh_folder_info_get_name (PhoshFolderInfo *self); +void phosh_folder_info_set_name (PhoshFolderInfo *self, const char *name); +GListModel *phosh_folder_info_get_app_infos (PhoshFolderInfo *self); +gboolean phosh_folder_info_contains (PhoshFolderInfo *self, GAppInfo *app_info); +gboolean phosh_folder_info_refilter (PhoshFolderInfo *self, const char *search); +void phosh_folder_info_add_app_info (PhoshFolderInfo *self, GAppInfo *app_info); +gboolean phosh_folder_info_remove_app_info (PhoshFolderInfo *self, GAppInfo *app_info); + +G_END_DECLS diff --git a/src/gnome-shell-manager.c b/src/gnome-shell-manager.c new file mode 100644 index 000000000..854aa2448 --- /dev/null +++ b/src/gnome-shell-manager.c @@ -0,0 +1,805 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * Evangelos Ribeiro Tzaras + */ + +#define G_LOG_DOMAIN "phosh-gnome-shell-manager" + +#include "phosh-config.h" + +#include "gnome-shell-manager.h" +#include "shell-priv.h" +#include "lockscreen-manager.h" + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#define PHOSH_VERSION_SUFFIX " (phosh " PHOSH_VERSION ")" + +/** + * PhoshGnomeShellManager: + * + * Provides the org.gnome.Shell DBus interface + * + */ + +#define GNOME_SHELL_DBUS_NAME "org.gnome.Shell" + +static void phosh_gnome_shell_manager_gnome_shell_iface_init (PhoshDBusGnomeShellIface *iface); + +enum { + PROP_0, + PROP_ACTION_MODE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshGnomeShellManager { + PhoshDBusGnomeShellSkeleton parent; + + GHashTable *info_by_action; + guint last_action_id; + int dbus_name_id; + PhoshShellActionMode action_mode; + + GSettings *keyboard_settings; + gboolean do_repeat; + guint repeat_delay_ms; + guint repeat_interval_ms; + + gboolean overview_active; +} PhoshGnomeShellManager; + +G_DEFINE_TYPE_WITH_CODE (PhoshGnomeShellManager, + phosh_gnome_shell_manager, + PHOSH_DBUS_TYPE_GNOME_SHELL_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_GNOME_SHELL, + phosh_gnome_shell_manager_gnome_shell_iface_init)); + +static void accelerator_activated_action (GSimpleAction *action, GVariant *param, gpointer data); + +typedef struct _AcceleratorInfo { + guint action_id; + char *accelerator; + char *sender; + guint mode_flags; + guint grab_flags; + guint repeat_id; +} AcceleratorInfo; + +static void +remove_action_entries (char *accelerator) +{ + GStrv action_names = (char*[]){ accelerator, NULL }; + + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + action_names); +} + + +static void +free_accelerator_info_from_hash_table (gpointer data) +{ + AcceleratorInfo *info = (AcceleratorInfo *) data; + g_return_if_fail (info != NULL); + + remove_action_entries (info->accelerator); + g_clear_handle_id (&info->repeat_id, g_source_remove); + g_free (info->accelerator); + g_free (info->sender); + g_free (info); +} + +/* DBus handlers */ +static gboolean +handle_show_monitor_labels (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + GVariant *arg_params) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + g_debug ("DBus show monitor labels"); + + phosh_dbus_gnome_shell_complete_show_monitor_labels ( + skeleton, invocation); + + return TRUE; +} + + +static gboolean +handle_hide_monitor_labels (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + g_debug ("DBus hide monitor labels"); + + phosh_dbus_gnome_shell_complete_hide_monitor_labels ( + skeleton, invocation); + + return TRUE; +} + + + + +static gboolean +handle_show_osd (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + GVariant *arg_params) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + g_autofree char *connector = NULL, *icon = NULL, *label = NULL; + double level = 0.0, maxlevel = 1.0; + gboolean has_level; + g_auto (GVariantDict) dict = G_VARIANT_DICT_INIT (NULL); + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + + g_variant_dict_init (&dict, arg_params); + g_variant_dict_lookup (&dict, "connector", "s", &connector); + g_variant_dict_lookup (&dict, "icon", "s", &icon); + g_variant_dict_lookup (&dict, "label", "s", &label); + has_level = g_variant_dict_lookup (&dict, "level", "d", &level); + g_variant_dict_lookup (&dict, "max_level", "d", &maxlevel); + + if (!has_level) + level = -1.0; + + phosh_shell_show_osd (phosh_shell_get_default (), connector, icon, label, level, maxlevel); + + phosh_dbus_gnome_shell_complete_show_osd (skeleton, invocation); + + return TRUE; +} + + +static guint +grab_single_accelerator (PhoshGnomeShellManager *self, + const char *accelerator, + guint mode_flags, + guint grab_flags, + const char *sender, + GError **error) +{ + AcceleratorInfo *info; + const GActionEntry action_entries[] = { + { .name = accelerator, .activate = accelerator_activated_action, "b" }, + }; + + g_assert (PHOSH_IS_GNOME_SHELL_MANAGER (self)); + + if (self->last_action_id == G_MAXUINT) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_ADDRESS_IN_USE, "All action ids taken"); + return 0; + } + + /* this should never happen */ + if (g_hash_table_contains (self->info_by_action, GUINT_TO_POINTER (self->last_action_id + 1))) { + g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED, "action id %d already taken", self->last_action_id + 1); + return 0; + } + + info = g_new0 (AcceleratorInfo, 1); + info->accelerator = g_strdup (accelerator); + info->action_id = ++(self->last_action_id); + info->sender = g_strdup (sender); + info->mode_flags = mode_flags; + info->grab_flags = grab_flags; + + g_debug ("Using action id %d for accelerator %s", info->action_id, info->accelerator); + g_hash_table_insert (self->info_by_action, GUINT_TO_POINTER (info->action_id), info); + + if (g_strcmp0 (accelerator, "XF86PowerOff")) { + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + action_entries, + G_N_ELEMENTS (action_entries), + info); + } else { + /* + * FIXME: Don't allow binding of power keys so we can blank the screen + * See https://gitlab.gnome.org/GNOME/gnome-settings-daemon/-/issues/703 + * We don't return an error as we want g-s-d to handle all the other keys + */ + g_debug ("Skipping power key grab"); + } + + g_assert (info->action_id > 0); + return info->action_id; +} + + +static gboolean +handle_grab_accelerator (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + const char *arg_accelerator, + guint arg_modeFlags, + guint arg_grabFlags) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + g_autoptr (GError) error = NULL; + const char *sender; + guint action_id; + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + g_debug ("DBus grab accelerator %s", arg_accelerator); + + sender = g_dbus_method_invocation_get_sender (invocation); + + action_id = grab_single_accelerator (self, + arg_accelerator, + arg_modeFlags, + arg_grabFlags, + sender, + &error); + if (action_id == 0) { + g_warning ("Error trying to grab accelerator %s: %s", arg_accelerator, error->message); + g_dbus_method_invocation_return_error (invocation, + error->domain, + error->code, + "%s", + error->message); + return TRUE; + } + + phosh_dbus_gnome_shell_complete_grab_accelerator ( + skeleton, invocation, action_id); + + return TRUE; +} + + +static gboolean +handle_grab_accelerators (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + GVariant *arg_accelerators) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + g_autoptr (GVariantBuilder) builder = NULL; + g_autoptr (GVariantIter) arg_iter = NULL; + char *accelerator_name; + guint accelerator_mode_flags; + guint accelerator_grab_flags; + g_autoptr (GError) error = NULL; + const char *sender; + gboolean conflict = FALSE; + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + g_debug ("DBus grab accelerators"); + + sender = g_dbus_method_invocation_get_sender (invocation); + + builder = g_variant_builder_new (G_VARIANT_TYPE ("au")); + g_variant_get (arg_accelerators, "a(suu)", &arg_iter); + + while (g_variant_iter_loop (arg_iter, "(&suu)", + &accelerator_name, + &accelerator_mode_flags, + &accelerator_grab_flags)) { + guint action_id; + + action_id = grab_single_accelerator (self, + accelerator_name, + accelerator_mode_flags, + accelerator_grab_flags, + sender, + &error); + if (action_id == 0) { + g_warning ("Error trying to grab accelerator %s: %s", accelerator_name, error->message); + g_dbus_method_invocation_return_error (invocation, + error->domain, + error->code, + "%s", + error->message); + conflict = TRUE; + break; + } + + g_variant_builder_add (builder, "u", action_id); + } + + if (conflict) { /* clean up existing bindings */ + g_autoptr (GVariant) unroll_ids = g_variant_builder_end (builder); + gsize n = g_variant_n_children (unroll_ids); + + for (gsize i = 0; i < n; ++i) { + guint action_id; + + g_variant_get_child (unroll_ids, i, "u", &action_id); + g_hash_table_remove (self->info_by_action, GUINT_TO_POINTER (action_id)); + } + } else { /* success */ + phosh_dbus_gnome_shell_complete_grab_accelerators ( + skeleton, invocation, g_variant_builder_end (builder)); + } + + return TRUE; +} + + +static gboolean +handle_ungrab_accelerator (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + guint arg_action) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + AcceleratorInfo *info; + gboolean success = FALSE; + const char *sender; + + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + g_debug ("DBus ungrab accelerator (id %u)", arg_action); + + sender = g_dbus_method_invocation_get_sender (invocation); + info = g_hash_table_lookup (self->info_by_action, GUINT_TO_POINTER (arg_action)); + + if (info != NULL) { + if (g_strcmp0 (info->sender, sender) == 0) { + g_hash_table_remove (self->info_by_action, GUINT_TO_POINTER (info->action_id)); + success = TRUE; + } else { + g_debug ("Ungrab not allowed: Sender %s not allowed to ungrab (grabbed by %s)", + sender, info->sender); + } + } + + phosh_dbus_gnome_shell_complete_ungrab_accelerator ( + skeleton, invocation, success); + + return TRUE; +} + + +static gboolean +handle_ungrab_accelerators (PhoshDBusGnomeShell *skeleton, + GDBusMethodInvocation *invocation, + GVariant *arg_actions) +{ + gsize n; + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + AcceleratorInfo *info; + gboolean success = TRUE; + const char *sender; + g_return_val_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self), FALSE); + + sender = g_dbus_method_invocation_get_sender (invocation); + n = g_variant_n_children (arg_actions); + g_debug ("DBus ungrab %" G_GSIZE_FORMAT " accelerators", n); + + for (gsize i = 0; i < n; i++) { + guint arg_action; + g_variant_get_child (arg_actions, i, "u", &arg_action); + info = g_hash_table_lookup (self->info_by_action, GUINT_TO_POINTER (arg_action)); + if (info == NULL) { + g_warning ("Can't ungrab: No accelerator (id %u) found", arg_action); + success = FALSE; + continue; + } + + if (g_strcmp0 (info->sender, sender) != 0) { + g_warning ("Ungrab (id %u) not allowed: Sender %s not allowed to ungrab (grabbed by %s)", + arg_action, sender, info->sender); + success = FALSE; + continue; + } + g_hash_table_remove (self->info_by_action, GUINT_TO_POINTER (info->action_id)); + } + phosh_dbus_gnome_shell_complete_ungrab_accelerators ( + skeleton, invocation, success); + + return TRUE; +} + + +static void +accelerator_activated (PhoshDBusGnomeShell *skeleton, + guint arg_action, + GVariant *arg_parameters) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (skeleton); + + g_return_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self)); + g_debug ("DBus emitting accelerator activated for action %u", arg_action); + + phosh_dbus_gnome_shell_emit_accelerator_activated ( + skeleton, arg_action, arg_parameters); + +} + + +static void +on_keyboard_setting_changed (PhoshGnomeShellManager *self, + const char *key, + GSettings *settings) +{ + g_assert (PHOSH_IS_GNOME_SHELL_MANAGER (self)); + + self->do_repeat = g_settings_get_boolean (self->keyboard_settings, "repeat"); + self->repeat_delay_ms = g_settings_get_uint (self->keyboard_settings, "delay"); + self->repeat_interval_ms = g_settings_get_uint (self->keyboard_settings, "repeat-interval"); + + g_debug ("Key repeat %sabled (delay: %u, interval: %u)", + self->do_repeat ? "en" : "dis", self->repeat_delay_ms, self->repeat_interval_ms); +} + + +static void +phosh_gnome_shell_manager_gnome_shell_iface_init (PhoshDBusGnomeShellIface *iface) +{ + iface->handle_show_monitor_labels = handle_show_monitor_labels; + iface->handle_hide_monitor_labels = handle_hide_monitor_labels; + iface->handle_show_osd = handle_show_osd; + iface->handle_grab_accelerator = handle_grab_accelerator; + iface->handle_grab_accelerators = handle_grab_accelerators; + iface->handle_ungrab_accelerator = handle_ungrab_accelerator; + iface->handle_ungrab_accelerators = handle_ungrab_accelerators; +} + + +static char * +get_version (void) +{ + /* We don't want to infringe on the GNOME version and not upset + anyone so make it very visible this is not GNOMEs version */ + return g_strdup_printf ("%d%s", GNOME_DESKTOP_PLATFORM_VERSION, PHOSH_VERSION_SUFFIX); +} + + +static PhoshShellActionMode +get_action_mode (PhoshShellStateFlags state) +{ + PhoshShell *shell = phosh_shell_get_default (); + + if (state & PHOSH_STATE_LOCKED) { + PhoshLockscreenManager *lockscreen_manager = phosh_shell_get_lockscreen_manager (shell); + PhoshLockscreenPage page = phosh_lockscreen_manager_get_page (lockscreen_manager); + + if (page == PHOSH_LOCKSCREEN_PAGE_UNLOCK) + return PHOSH_SHELL_ACTION_MODE_UNLOCK_SCREEN; + else + return PHOSH_SHELL_ACTION_MODE_LOCK_SCREEN; + } + + if (state & PHOSH_STATE_MODAL_SYSTEM_PROMPT) + return PHOSH_SHELL_ACTION_MODE_SYSTEM_MODAL; + + if (state & PHOSH_STATE_OVERVIEW) + return PHOSH_SHELL_ACTION_MODE_OVERVIEW; + + return PHOSH_SHELL_ACTION_MODE_NORMAL; +} + + +static void +do_activate_accelerator (AcceleratorInfo *info) +{ + PhoshGnomeShellManager *self = phosh_gnome_shell_manager_get_default (); + g_autoptr (GVariantBuilder) builder = NULL; + GVariant *parameters; + + g_assert (info); + + if ((info->mode_flags & self->action_mode) == 0) { + g_autofree char *str_shell_mode = g_flags_to_string (PHOSH_TYPE_SHELL_ACTION_MODE, + self->action_mode); + g_autofree char *str_grabbed_mode = g_flags_to_string (PHOSH_TYPE_SHELL_ACTION_MODE, + info->mode_flags); + g_debug ("Accelerator registered for mode %s, but shell is currently in %s", + str_grabbed_mode, + str_shell_mode); + return; + } + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + /* + * Fill some dummy values + */ + g_variant_builder_add (builder, "{sv}", "device-id", g_variant_new_uint32 (0)); + g_variant_builder_add (builder, "{sv}", "timestamp", g_variant_new_uint32 (GDK_CURRENT_TIME)); + g_variant_builder_add (builder, "{sv}", "action-mode", g_variant_new_uint32 (0)); + g_variant_builder_add (builder, "{sv}", "device-id", g_variant_new_string ("/dev/input/event0")); + parameters = g_variant_builder_end (builder); + + accelerator_activated (PHOSH_DBUS_GNOME_SHELL (self), + info->action_id, + parameters); +} + + +static gboolean +on_accelerator_repeat (gpointer data) +{ + AcceleratorInfo *info = data; + + g_assert (info); + g_assert (info->action_id); + + do_activate_accelerator (info); + + return G_SOURCE_CONTINUE; +} + + +static gboolean +on_accelerator_repeat_delay (gpointer data) +{ + PhoshGnomeShellManager *self = phosh_gnome_shell_manager_get_default (); + AcceleratorInfo *info = data; + g_autofree char *source_name = g_strdup_printf ("[phosh] key repeat for id %u", info->action_id); + + g_assert (info); + g_assert (info->action_id); + + do_activate_accelerator (info); + + info->repeat_id = g_timeout_add (self->repeat_interval_ms, on_accelerator_repeat, info); + g_source_set_name_by_id (info->repeat_id, source_name); + + return G_SOURCE_REMOVE; +} + + +static void +accelerator_activated_action (GSimpleAction *action, + GVariant *param, + gpointer data) +{ + AcceleratorInfo *info = (AcceleratorInfo *) data; + PhoshGnomeShellManager *self = phosh_gnome_shell_manager_get_default (); + gboolean press = g_variant_get_boolean (param); + uint32_t action_id; + + action_id = info->action_id; + if (!press) { + g_debug ("accelerator released for id %u", action_id); + g_clear_handle_id (&info->repeat_id, g_source_remove); + return; + } + g_debug ("accelerator action activated for id %u", action_id); + + if ((info->grab_flags & PHOSH_SHELL_KEY_BINDING_IGNORE_AUTOREPEAT) == 0 && self->do_repeat) { + g_autofree char *source_name = g_strdup_printf ("[phosh] key-repeat-delay for %u", action_id); + g_debug ("setting up accelerator autorepeat for id %u", action_id); + info->repeat_id = g_timeout_add (self->repeat_delay_ms, on_accelerator_repeat_delay, info); + g_source_set_name_by_id (info->repeat_id, source_name); + } + + do_activate_accelerator (info); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (user_data); + + g_debug ("Acquired name %s", name); + g_return_if_fail (PHOSH_IS_GNOME_SHELL_MANAGER (self)); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_autoptr (GError) err = NULL; + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (user_data); + PhoshSessionManager *sm; + gboolean success; + + success = g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/gnome/Shell", + &err); + if (!success) { + g_warning ("Failed to export shell interface: %s", err->message); + return; + } + + sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + phosh_session_manager_export_end_session (sm, connection); +} + + +static gboolean +transform_state_to_action_mode (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer unused) +{ + PhoshShellStateFlags shell_state = g_value_get_flags (from_value); + PhoshShellActionMode action_mode = get_action_mode (shell_state); + + g_value_set_flags (to_value, action_mode); + return TRUE; +} + + +static void +on_shell_state_changed (PhoshGnomeShellManager *self, GParamSpec *pspec, PhoshShell *shell) +{ + gboolean overview_active; + + g_assert (PHOSH_IS_SHELL (shell)); + g_assert (PHOSH_IS_GNOME_SHELL_MANAGER (self)); + + overview_active = !!(phosh_shell_get_state (shell) & PHOSH_STATE_OVERVIEW); + if (overview_active == self->overview_active) + return; + + self->overview_active = overview_active; + g_object_set (G_OBJECT (self), "overview-active", self->overview_active, NULL); +} + + +static void +phosh_gnome_shell_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (object); + + switch (property_id) { + case PROP_ACTION_MODE: + self->action_mode = g_value_get_flags (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_gnome_shell_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (object); + + switch (property_id) { + case PROP_ACTION_MODE: + g_value_set_flags (value, self->action_mode); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_gnome_shell_manager_dispose (GObject *object) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (object); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_pointer (&self->info_by_action, g_hash_table_unref); + g_clear_object (&self->keyboard_settings); + + G_OBJECT_CLASS (phosh_gnome_shell_manager_parent_class)->dispose (object); +} + + +static void +phosh_gnome_shell_manager_constructed (GObject *object) +{ + PhoshGnomeShellManager *self = PHOSH_GNOME_SHELL_MANAGER (object); + PhoshShell *shell = phosh_shell_get_default (); + + G_OBJECT_CLASS (phosh_gnome_shell_manager_parent_class)->constructed (object); + + g_object_bind_property_full (shell, "shell-state", + object, "shell-action-mode", + G_BINDING_SYNC_CREATE, + (GBindingTransformFunc) transform_state_to_action_mode, + NULL, NULL, NULL); + + g_signal_connect_object (shell, "notify::shell-state", + G_CALLBACK (on_shell_state_changed), + self, + G_CONNECT_SWAPPED); + + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + GNOME_SHELL_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); +} + + +static void +phosh_gnome_shell_manager_class_init (PhoshGnomeShellManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_gnome_shell_manager_get_property; + object_class->set_property = phosh_gnome_shell_manager_set_property; + object_class->constructed = phosh_gnome_shell_manager_constructed; + object_class->dispose = phosh_gnome_shell_manager_dispose; + + props[PROP_ACTION_MODE] = + g_param_spec_flags ("shell-action-mode", + "Shell Action Mode", + "The active action mode (used for keygrabbing)", + PHOSH_TYPE_SHELL_ACTION_MODE, + PHOSH_SHELL_ACTION_MODE_NONE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_gnome_shell_manager_init (PhoshGnomeShellManager *self) +{ + g_autofree char *version = get_version (); + + self->info_by_action = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + free_accelerator_info_from_hash_table); + self->last_action_id = 0; + + self->keyboard_settings = g_settings_new ("org.gnome.desktop.peripherals.keyboard"); + + g_object_connect (self->keyboard_settings, + "swapped-signal::changed::repeat", G_CALLBACK (on_keyboard_setting_changed), self, + "swapped-signal::changed::repeat-interval", G_CALLBACK (on_keyboard_setting_changed), self, + "swapped-signal::changed::delay", G_CALLBACK (on_keyboard_setting_changed), self, + NULL); + on_keyboard_setting_changed (self, NULL, self->keyboard_settings); + + g_object_set (G_OBJECT (self), "shell-version", version, NULL); +} + +/** + * phosh_gnome_shell_manager_get_default: + * + * Get the shell manager singleton + * + * Returns:(transfer none): The shell manager singleton + */ +PhoshGnomeShellManager * +phosh_gnome_shell_manager_get_default (void) +{ + static PhoshGnomeShellManager *instance; + + if (instance == NULL) { + g_debug ("Creating gnome shell DBus manager"); + instance = g_object_new (PHOSH_TYPE_GNOME_SHELL_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *) &instance); + } + return instance; +} diff --git a/src/gnome-shell-manager.h b/src/gnome-shell-manager.h new file mode 100644 index 000000000..530da83f3 --- /dev/null +++ b/src/gnome-shell-manager.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * Evangelos Ribeiro Tzaras + * + * ShellActionMode is from GNOME Shell's shell-action-modes.h + * which is GPL-2.0-or-later and authored by + * Florian Müllner + */ +#pragma once + +#include "dbus/phosh-gnome-shell-dbus.h" +#include + +/** + * ShellActionMode: + * @PHOSH_SHELL_ACTION_MODE_NONE: block action + * @PHOSH_SHELL_ACTION_MODE_NORMAL: allow action when in window mode, + * e.g. when the focus is in an application window + * @PHOSH_SHELL_ACTION_MODE_OVERVIEW: allow action while the overview + * is active + * @PHOSH_SHELL_ACTION_MODE_LOCK_SCREEN: allow action when the screen + * is locked, e.g. when the screen shield is shown + * @PHOSH_SHELL_ACTION_MODE_UNLOCK_SCREEN: allow action in the unlock + * dialog + * @PHOSH_SHELL_ACTION_MODE_LOGIN_SCREEN: allow action in the login screen + * @PHOSH_SHELL_ACTION_MODE_SYSTEM_MODAL: allow action when a system modal + * dialog (e.g. authentication or session dialogs) is open + * @PHOSH_SHELL_ACTION_MODE_LOOKING_GLASS: allow action in looking glass + * @PHOSH_SHELL_ACTION_MODE_POPUP: allow action while a shell menu is open + * @PHOSH_SHELL_ACTION_MODE_ALL: always allow action + * + * Controls in which GNOME Shell states an action (like keybindings and gestures) + * should be handled. +*/ +typedef enum { + PHOSH_SHELL_ACTION_MODE_NONE = 0, + PHOSH_SHELL_ACTION_MODE_NORMAL = 1 << 0, + PHOSH_SHELL_ACTION_MODE_OVERVIEW = 1 << 1, + PHOSH_SHELL_ACTION_MODE_LOCK_SCREEN = 1 << 2, + PHOSH_SHELL_ACTION_MODE_UNLOCK_SCREEN = 1 << 3, + PHOSH_SHELL_ACTION_MODE_LOGIN_SCREEN = 1 << 4, + PHOSH_SHELL_ACTION_MODE_SYSTEM_MODAL = 1 << 5, + PHOSH_SHELL_ACTION_MODE_LOOKING_GLASS = 1 << 6, + PHOSH_SHELL_ACTION_MODE_POPUP = 1 << 7, + + PHOSH_SHELL_ACTION_MODE_ALL = ~0, +} PhoshShellActionMode; + +/** + * ShellKeyBindingFlags: + * @PHOSH_SHELL_KEY_BINDING_NONE: none + * @HPOSH_SHELL_KEY_BINDING_PER_WINDOW: per-window + * @PHOSH_SHELL_KEY_BINDING_BUILTIN: built-in + * @PHOSH_SHELL_KEY_BINDING_IS_REVERSED: is reversed + * @PHOSH_SHELL_KEY_BINDING_NON_MASKABLE: always active + * @PHOSH_SHELL_KEY_BINDING_IGNORE_AUTOREPEAT: ignore autorepeat + * @PHOSH_SHELL_KEY_BINDING_NO_AUTO_GRAB: not grabbed automatically + */ +typedef enum +{ + /* not implemented */ + PHOSH_SHELL_KEY_BINDING_NONE, + PHOSH_SHELL_KEY_BINDING_PER_WINDOW = 1 << 0, + PHOSH_SHELL_KEY_BINDING_BUILTIN = 1 << 1, + PHOSH_SHELL_KEY_BINDING_IS_REVERSED = 1 << 2, + PHOSH_SHELL_KEY_BINDING_NON_MASKABLE = 1 << 3, + PHOSH_SHELL_KEY_BINDING_NO_AUTO_GRAB = 1 << 5, + /* implemented */ + PHOSH_SHELL_KEY_BINDING_IGNORE_AUTOREPEAT = 1 << 4, +} PhoshShellKeyBindingFlags; + +G_BEGIN_DECLS + +#define PHOSH_TYPE_GNOME_SHELL_MANAGER (phosh_gnome_shell_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshGnomeShellManager, phosh_gnome_shell_manager, PHOSH, GNOME_SHELL_MANAGER, + PhoshDBusGnomeShellSkeleton) + +PhoshGnomeShellManager *phosh_gnome_shell_manager_get_default (void); + +G_END_DECLS diff --git a/src/gtk-list-models/gtkfilterlistmodel.c b/src/gtk-list-models/gtkfilterlistmodel.c new file mode 100644 index 000000000..022194322 --- /dev/null +++ b/src/gtk-list-models/gtkfilterlistmodel.c @@ -0,0 +1,711 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "phosh-config.h" + +#include "gtkfilterlistmodel.h" + +#include "gtkrbtreeprivate.h" + +#include + +/** + * SECTION:gtkfilterlistmodel + * @title: GtkFilterListModel + * @short_description: A list model that filters its items + * @see_also: #GListModel + * + * #GtkFilterListModel is a list model that filters a given other + * listmodel. + * It hides some elements from the other model according to + * criteria given by a #GtkFilterListModelFilterFunc. + */ + +enum { + PROP_0, + PROP_HAS_FILTER, + PROP_ITEM_TYPE, + PROP_MODEL, + NUM_PROPERTIES +}; + +typedef struct _FilterNode FilterNode; +typedef struct _FilterAugment FilterAugment; + +struct _FilterNode +{ + guint visible : 1; +}; + +struct _FilterAugment +{ + guint n_items; + guint n_visible; +}; + +struct _GtkFilterListModel +{ + GObject parent_instance; + + GType item_type; + GListModel *model; + GtkFilterListModelFilterFunc filter_func; + gpointer user_data; + GDestroyNotify user_destroy; + + GtkRbTree *items; /* NULL if filter_func == NULL */ +}; + +struct _GtkFilterListModelClass +{ + GObjectClass parent_class; +}; + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +static FilterNode * +gtk_filter_list_model_get_nth_filtered (GtkRbTree *tree, + guint position, + guint *out_unfiltered) +{ + FilterNode *node, *tmp; + guint unfiltered; + + node = gtk_rb_tree_get_root (tree); + unfiltered = 0; + + while (node) + { + tmp = gtk_rb_tree_node_get_left (node); + if (tmp) + { + FilterAugment *aug = gtk_rb_tree_get_augment (tree, tmp); + if (position < aug->n_visible) + { + node = tmp; + continue; + } + position -= aug->n_visible; + unfiltered += aug->n_items; + } + + if (node->visible) + { + if (position == 0) + break; + position--; + } + + unfiltered++; + + node = gtk_rb_tree_node_get_right (node); + } + + if (out_unfiltered) + *out_unfiltered = unfiltered; + + return node; +} + +static FilterNode * +gtk_filter_list_model_get_nth (GtkRbTree *tree, + guint position, + guint *out_filtered) +{ + FilterNode *node, *tmp; + guint filtered; + + node = gtk_rb_tree_get_root (tree); + filtered = 0; + + while (node) + { + tmp = gtk_rb_tree_node_get_left (node); + if (tmp) + { + FilterAugment *aug = gtk_rb_tree_get_augment (tree, tmp); + if (position < aug->n_items) + { + node = tmp; + continue; + } + position -= aug->n_items; + filtered += aug->n_visible; + } + + if (position == 0) + break; + + position--; + if (node->visible) + filtered++; + + node = gtk_rb_tree_node_get_right (node); + } + + if (out_filtered) + *out_filtered = filtered; + + return node; +} + +static GType +gtk_filter_list_model_get_item_type (GListModel *list) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (list); + + return self->item_type; +} + +static guint +gtk_filter_list_model_get_n_items (GListModel *list) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (list); + FilterAugment *aug; + FilterNode *node; + + if (self->model == NULL) + return 0; + + if (!self->items) + return g_list_model_get_n_items (self->model); + + node = gtk_rb_tree_get_root (self->items); + if (node == NULL) + return 0; + + aug = gtk_rb_tree_get_augment (self->items, node); + return aug->n_visible; +} + +static gpointer +gtk_filter_list_model_get_item (GListModel *list, + guint position) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (list); + guint unfiltered; + + if (self->model == NULL) + return NULL; + + if (self->items) + gtk_filter_list_model_get_nth_filtered (self->items, position, &unfiltered); + else + unfiltered = position; + + return g_list_model_get_item (self->model, unfiltered); +} + +static void +gtk_filter_list_model_model_init (GListModelInterface *iface) +{ + iface->get_item_type = gtk_filter_list_model_get_item_type; + iface->get_n_items = gtk_filter_list_model_get_n_items; + iface->get_item = gtk_filter_list_model_get_item; +} + +G_DEFINE_TYPE_WITH_CODE (GtkFilterListModel, gtk_filter_list_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_filter_list_model_model_init)) + +static gboolean +gtk_filter_list_model_run_filter (GtkFilterListModel *self, + guint position) +{ + gpointer item; + gboolean visible; + + item = g_list_model_get_item (self->model, position); + visible = self->filter_func (item, self->user_data); + g_object_unref (item); + + return visible; +} + +static guint +gtk_filter_list_model_add_items (GtkFilterListModel *self, + FilterNode *after, + guint position, + guint n_items) +{ + FilterNode *node; + guint i, n_visible; + + n_visible = 0; + + for (i = 0; i < n_items; i++) + { + node = gtk_rb_tree_insert_before (self->items, after); + node->visible = gtk_filter_list_model_run_filter (self, position + i); + if (node->visible) + n_visible++; + } + + return n_visible; +} + +static void +gtk_filter_list_model_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkFilterListModel *self) +{ + FilterNode *node; + guint i, filter_position, filter_removed, filter_added; + + if (self->items == NULL) + { + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + return; + } + + node = gtk_filter_list_model_get_nth (self->items, position, &filter_position); + + filter_removed = 0; + for (i = 0; i < removed; i++) + { + FilterNode *next = gtk_rb_tree_node_get_next (node); + if (node->visible) + filter_removed++; + gtk_rb_tree_remove (self->items, node); + node = next; + } + + filter_added = gtk_filter_list_model_add_items (self, node, position, added); + + if (filter_removed > 0 || filter_added > 0) + g_list_model_items_changed (G_LIST_MODEL (self), filter_position, filter_removed, filter_added); +} + +static void +gtk_filter_list_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_ITEM_TYPE: + self->item_type = g_value_get_gtype (value); + break; + + case PROP_MODEL: + gtk_filter_list_model_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_filter_list_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_HAS_FILTER: + g_value_set_boolean (value, self->items != NULL); + break; + + case PROP_ITEM_TYPE: + g_value_set_gtype (value, self->item_type); + break; + + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_filter_list_model_clear_model (GtkFilterListModel *self) +{ + if (self->model == NULL) + return; + + g_signal_handlers_disconnect_by_func (self->model, gtk_filter_list_model_items_changed_cb, self); + g_clear_object (&self->model); + if (self->items) + gtk_rb_tree_remove_all (self->items); +} + +static void +gtk_filter_list_model_dispose (GObject *object) +{ + GtkFilterListModel *self = GTK_FILTER_LIST_MODEL (object); + + gtk_filter_list_model_clear_model (self); + if (self->user_destroy) + self->user_destroy (self->user_data); + self->filter_func = NULL; + self->user_data = NULL; + self->user_destroy = NULL; + g_clear_pointer (&self->items, gtk_rb_tree_unref); + + G_OBJECT_CLASS (gtk_filter_list_model_parent_class)->dispose (object); +} + +static void +gtk_filter_list_model_class_init (GtkFilterListModelClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + + gobject_class->set_property = gtk_filter_list_model_set_property; + gobject_class->get_property = gtk_filter_list_model_get_property; + gobject_class->dispose = gtk_filter_list_model_dispose; + + /** + * GtkFilterListModel:has-filter: + * + * If a filter is set for this model + */ + properties[PROP_HAS_FILTER] = + g_param_spec_boolean ("has-filter", + N_("has filter"), + N_("If a filter is set for this model"), + FALSE, + G_PARAM_STATIC_STRINGS | G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkFilterListModel:item-type: + * + * The #GType for elements of this object + */ + properties[PROP_ITEM_TYPE] = + g_param_spec_gtype ("item-type", + N_("Item type"), + N_("The type of elements of this object"), + G_TYPE_OBJECT, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkFilterListModel:model: + * + * The model being filtered + */ + properties[PROP_MODEL] = + g_param_spec_object ("model", + N_("Model"), + N_("The model being filtered"), + G_TYPE_LIST_MODEL, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties); +} + +static void +gtk_filter_list_model_init (GtkFilterListModel *self) +{ +} + + +static void +gtk_filter_list_model_augment (GtkRbTree *filter, + gpointer _aug, + gpointer _node, + gpointer left, + gpointer right) +{ + FilterNode *node = _node; + FilterAugment *aug = _aug; + + aug->n_items = 1; + aug->n_visible = node->visible ? 1 : 0; + + if (left) + { + FilterAugment *left_aug = gtk_rb_tree_get_augment (filter, left); + aug->n_items += left_aug->n_items; + aug->n_visible += left_aug->n_visible; + } + if (right) + { + FilterAugment *right_aug = gtk_rb_tree_get_augment (filter, right); + aug->n_items += right_aug->n_items; + aug->n_visible += right_aug->n_visible; + } +} + +/** + * gtk_filter_list_model_new: + * @model: the model to sort + * @filter_func: (allow-none): filter function or %NULL to not filter items + * @user_data: user data passed to @filter_func + * @user_destroy: destroy notifier for @user_data + * + * Creates a new #GtkFilterListModel that will filter @model using the given + * @filter_func. + * + * Returns: a new #GtkFilterListModel + **/ +GtkFilterListModel * +gtk_filter_list_model_new (GListModel *model, + GtkFilterListModelFilterFunc filter_func, + gpointer user_data, + GDestroyNotify user_destroy) +{ + GtkFilterListModel *result; + + g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL); + + result = g_object_new (GTK_TYPE_FILTER_LIST_MODEL, + "item-type", g_list_model_get_item_type (model), + "model", model, + NULL); + + if (filter_func) + gtk_filter_list_model_set_filter_func (result, filter_func, user_data, user_destroy); + + return result; +} + +/** + * gtk_filter_list_model_new_for_type: + * @item_type: the type of the items that will be returned + * + * Creates a new empty filter list model set up to return items of type @item_type. + * It is up to the application to set a proper filter function and model to ensure + * the item type is matched. + * + * Returns: a new #GtkFilterListModel + **/ +GtkFilterListModel * +gtk_filter_list_model_new_for_type (GType item_type) +{ + g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL); + + return g_object_new (GTK_TYPE_FILTER_LIST_MODEL, + "item-type", item_type, + NULL); +} + +/** + * gtk_filter_list_model_set_filter_func: + * @self: a #GtkFilterListModel + * @filter_func: (allow-none): filter function or %NULL to not filter items + * @user_data: user data passed to @filter_func + * @user_destroy: destroy notifier for @user_data + * + * Sets the function used to filter items. The function will be called for every + * item and if it returns %TRUE the item is considered visible. + **/ +void +gtk_filter_list_model_set_filter_func (GtkFilterListModel *self, + GtkFilterListModelFilterFunc filter_func, + gpointer user_data, + GDestroyNotify user_destroy) +{ + gboolean was_filtered, will_be_filtered; + + g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); + g_return_if_fail (filter_func != NULL || (user_data == NULL && !user_destroy)); + + was_filtered = self->filter_func != NULL; + will_be_filtered = filter_func != NULL; + + if (!was_filtered && !will_be_filtered) + return; + + if (self->user_destroy) + self->user_destroy (self->user_data); + + self->filter_func = filter_func; + self->user_data = user_data; + self->user_destroy = user_destroy; + + if (!will_be_filtered) + { + g_clear_pointer (&self->items, gtk_rb_tree_unref); + } + else if (!was_filtered) + { + guint i, n_items; + + self->items = gtk_rb_tree_new (FilterNode, + FilterAugment, + gtk_filter_list_model_augment, + NULL, NULL); + if (self->model) + { + n_items = g_list_model_get_n_items (self->model); + for (i = 0; i < n_items; i++) + { + FilterNode *node = gtk_rb_tree_insert_before (self->items, NULL); + node->visible = TRUE; + } + } + } + + gtk_filter_list_model_refilter (self); + + if (was_filtered != will_be_filtered) + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_FILTER]); +} + +/** + * gtk_filter_list_model_set_model: + * @self: a #GtkFilterListModel + * @model: (allow-none): The model to be filtered + * + * Sets the model to be filtered. + * + * Note that GTK makes no effort to ensure that @model conforms to + * the item type of @self. It assumes that the caller knows what they + * are doing and have set up an appropriate filter function to ensure + * that item types match. + **/ +void +gtk_filter_list_model_set_model (GtkFilterListModel *self, + GListModel *model) +{ + guint removed, added; + + g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + /* Note: We don't check for matching item type here, we just assume the + * filter func takes care of filtering wrong items. */ + + if (self->model == model) + return; + + removed = g_list_model_get_n_items (G_LIST_MODEL (self)); + gtk_filter_list_model_clear_model (self); + + if (model) + { + self->model = g_object_ref (model); + g_signal_connect (model, "items-changed", G_CALLBACK (gtk_filter_list_model_items_changed_cb), self); + if (self->items) + added = gtk_filter_list_model_add_items (self, NULL, 0, g_list_model_get_n_items (model)); + else + added = g_list_model_get_n_items (model); + } + else + added = 0; + + if (removed > 0 || added > 0) + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); +} + +/** + * gtk_filter_list_model_get_model: + * @self: a #GtkFilterListModel + * + * Gets the model currently filtered or %NULL if none. + * + * Returns: (nullable) (transfer none): The model that gets filtered + **/ +GListModel * +gtk_filter_list_model_get_model (GtkFilterListModel *self) +{ + g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), NULL); + + return self->model; +} + +/** + * gtk_filter_list_model_has_filter: + * @self: a #GtkFilterListModel + * + * Checks if a filter function is currently set on @self + * + * Returns: %TRUE if a filter function is set + **/ +gboolean +gtk_filter_list_model_has_filter (GtkFilterListModel *self) +{ + g_return_val_if_fail (GTK_IS_FILTER_LIST_MODEL (self), FALSE); + + return self->filter_func != NULL; +} + +/** + * gtk_filter_list_model_refilter: + * @self: a #GtkFilterListModel + * + * Causes @self to refilter all items in the model. + * + * Calling this function is necessary when data used by the filter + * function has changed. + **/ +void +gtk_filter_list_model_refilter (GtkFilterListModel *self) +{ + FilterNode *node; + guint i, first_change, last_change; + guint n_is_visible, n_was_visible; + gboolean visible; + + g_return_if_fail (GTK_IS_FILTER_LIST_MODEL (self)); + + if (self->items == NULL || self->model == NULL) + return; + + first_change = G_MAXUINT; + last_change = 0; + n_is_visible = 0; + n_was_visible = 0; + for (i = 0, node = gtk_rb_tree_get_first (self->items); + node != NULL; + i++, node = gtk_rb_tree_node_get_next (node)) + { + visible = gtk_filter_list_model_run_filter (self, i); + if (visible == node->visible) + { + if (visible) + { + n_is_visible++; + n_was_visible++; + } + continue; + } + + node->visible = visible; + gtk_rb_tree_node_mark_dirty (node); + first_change = MIN (n_is_visible, first_change); + if (visible) + n_is_visible++; + else + n_was_visible++; + last_change = MAX (n_is_visible, last_change); + } + + if (first_change <= last_change) + { + g_list_model_items_changed (G_LIST_MODEL (self), + first_change, + last_change - first_change + n_was_visible - n_is_visible, + last_change - first_change); + } +} + diff --git a/src/gtk-list-models/gtkfilterlistmodel.h b/src/gtk-list-models/gtkfilterlistmodel.h new file mode 100644 index 000000000..f241809f1 --- /dev/null +++ b/src/gtk-list-models/gtkfilterlistmodel.h @@ -0,0 +1,74 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_FILTER_LIST_MODEL_H__ +#define __GTK_FILTER_LIST_MODEL_H__ + + +#include +#include + + +G_BEGIN_DECLS + +#define GTK_TYPE_FILTER_LIST_MODEL (gtk_filter_list_model_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkFilterListModel, gtk_filter_list_model, GTK, FILTER_LIST_MODEL, GObject) + +/** + * GtkFilterListModelFilterFunc: + * @item: (type GObject): The item that may be filtered + * @user_data: user data + * + * User function that is called to determine if the @item of the original model should be visible. + * If it should be visible, this function must return %TRUE. If the model should filter out the + * @item, %FALSE must be returned. + * + * Returns: %TRUE to keep the item around + */ +typedef gboolean (* GtkFilterListModelFilterFunc) (gpointer item, gpointer user_data); + +GDK_AVAILABLE_IN_ALL +GtkFilterListModel * gtk_filter_list_model_new (GListModel *model, + GtkFilterListModelFilterFunc filter_func, + gpointer user_data, + GDestroyNotify user_destroy); +GDK_AVAILABLE_IN_ALL +GtkFilterListModel * gtk_filter_list_model_new_for_type (GType item_type); + +GDK_AVAILABLE_IN_ALL +void gtk_filter_list_model_set_filter_func (GtkFilterListModel *self, + GtkFilterListModelFilterFunc filter_func, + gpointer user_data, + GDestroyNotify user_destroy); +GDK_AVAILABLE_IN_ALL +void gtk_filter_list_model_set_model (GtkFilterListModel *self, + GListModel *model); +GDK_AVAILABLE_IN_ALL +GListModel * gtk_filter_list_model_get_model (GtkFilterListModel *self); +GDK_AVAILABLE_IN_ALL +gboolean gtk_filter_list_model_has_filter (GtkFilterListModel *self); + +GDK_AVAILABLE_IN_ALL +void gtk_filter_list_model_refilter (GtkFilterListModel *self); + +G_END_DECLS + +#endif /* __GTK_FILTER_LIST_MODEL_H__ */ diff --git a/src/gtk-list-models/gtkrbtree.c b/src/gtk-list-models/gtkrbtree.c new file mode 100644 index 000000000..98aa546fa --- /dev/null +++ b/src/gtk-list-models/gtkrbtree.c @@ -0,0 +1,798 @@ +/* gtkrbtree.c + * Copyright (C) 2000 Red Hat, Inc., Jonathan Blandford + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +#include "phosh-config.h" + +#include "gtkrbtreeprivate.h" + +/* Define the following to print adds and removals to stdout. + * The format of the printout will be suitable for addition as a new test to + * testsuite/gtk/rbtree-crash.c + * by just grepping the printouts from the relevant rbtree. + * + * This is meant to be a trivial way to add rbtree tests to the testsuite. + */ +#undef DUMP_MODIFICATION + +typedef struct _GtkRbNode GtkRbNode; + +struct _GtkRbTree +{ + guint ref_count; + + gsize element_size; + gsize augment_size; + GtkRbTreeAugmentFunc augment_func; + GDestroyNotify clear_func; + GDestroyNotify clear_augment_func; + + GtkRbNode *root; +}; + +struct _GtkRbNode +{ + guint red :1; + guint dirty :1; + + GtkRbNode *left; + GtkRbNode *right; + /* The difference between tree and parent here is that we OR the tree with 1 and because + * pointers are always multiples of 4, we can know if we've stored a parent or the tree here */ + union { + gpointer parent_or_tree; + GtkRbNode *parent; + GtkRbTree *tree; + }; +}; + +#define NODE_FROM_POINTER(ptr) ((GtkRbNode *) ((ptr) ? (((guchar *) (ptr)) - sizeof (GtkRbNode)) : NULL)) +#define NODE_TO_POINTER(node) ((gpointer) ((node) ? (((guchar *) (node)) + sizeof (GtkRbNode)) : NULL)) +#define NODE_TO_AUG_POINTER(tree, node) ((gpointer) ((node) ? (((guchar *) (node)) + sizeof (GtkRbNode) + (tree)->element_size) : NULL)) + +static inline gboolean +is_root (GtkRbNode *node) +{ + return GPOINTER_TO_SIZE (node->parent_or_tree) & 1 ? TRUE : FALSE; +} + +static inline GtkRbNode * +parent (GtkRbNode *node) +{ + if (is_root (node)) + return NULL; + else + return node->parent; +} + +static GtkRbTree * +tree (GtkRbNode *node) +{ + while (!is_root (node)) + node = parent (node); + + return GSIZE_TO_POINTER (GPOINTER_TO_SIZE (node->tree) & ~1); +} + +static void +set_parent (GtkRbTree *tree, + GtkRbNode *node, + GtkRbNode *new_parent) +{ + + if (new_parent != NULL) + { + node->parent = new_parent; + } + else + { + node->tree = GSIZE_TO_POINTER (GPOINTER_TO_SIZE (tree) | 1); + tree->root = node; + } +} + +static inline gsize +gtk_rb_node_get_size (GtkRbTree *tree) +{ + return sizeof (GtkRbNode) + tree->element_size + tree->augment_size; +} + +static GtkRbNode * +gtk_rb_node_new (GtkRbTree *tree) +{ + GtkRbNode *result; + + result = g_slice_alloc0 (gtk_rb_node_get_size (tree)); + + result->red = TRUE; + result->dirty = TRUE; + + return result; +} + +static void +gtk_rb_node_free (GtkRbTree *tree, + GtkRbNode *node) +{ + if (tree->clear_func) + tree->clear_func (NODE_TO_POINTER (node)); + if (tree->clear_augment_func) + tree->clear_augment_func (NODE_TO_AUG_POINTER (tree, node)); + + g_slice_free1 (gtk_rb_node_get_size (tree), node); +} + +static void +gtk_rb_node_free_deep (GtkRbTree *tree, + GtkRbNode *node) +{ + GtkRbNode *right = node->right; + + if (node->left) + gtk_rb_node_free_deep (tree, node->left); + + gtk_rb_node_free (tree, node); + + if (right) + gtk_rb_node_free_deep (tree, right); +} + +static void +gtk_rb_node_mark_dirty (GtkRbNode *node, + gboolean mark_parent) +{ + if (node->dirty) + return; + + node->dirty = TRUE; + + if (mark_parent && parent (node)) + gtk_rb_node_mark_dirty (parent (node), TRUE); +} + +static void +gtk_rb_node_clean (GtkRbTree *tree, + GtkRbNode *node) +{ + if (!node->dirty) + return; + + node->dirty = FALSE; + if (tree->augment_func) + tree->augment_func (tree, + NODE_TO_AUG_POINTER (tree, node), + NODE_TO_POINTER (node), + NODE_TO_POINTER (node->left), + NODE_TO_POINTER (node->right)); +} + +static GtkRbNode * +gtk_rb_node_get_first (GtkRbNode *node) +{ + while (node->left) + node = node->left; + + return node; +} + +static GtkRbNode * +gtk_rb_node_get_last (GtkRbNode *node) +{ + while (node->right) + node = node->right; + + return node; +} + +static GtkRbNode * +gtk_rb_node_get_previous (GtkRbNode *node) +{ + GtkRbNode *p; + + if (node->left) + return gtk_rb_node_get_last (node->left); + + for (p = parent (node); p != NULL; p = parent (node)) + { + if (p->right == node) + return p; + + node = p; + } + + return NULL; +} + +static GtkRbNode * +gtk_rb_node_get_next (GtkRbNode *node) +{ + GtkRbNode *p; + + if (node->right) + return gtk_rb_node_get_first (node->right); + + for (p = parent (node); p != NULL; p = parent (node)) + { + if (p->left == node) + return p; + + node = p; + } + + return NULL; +} + +#ifdef DUMP_MODIFICATION +static guint +position (GtkRbTree *tree, + GtkRbNode *node) +{ + GtkRbNode *n; + guint i; + + i = 0; + for (n = gtk_rb_node_get_first (tree->root); + n != node; + n = gtk_rb_node_get_next (n)) + i++; + + return i; +} +#endif + +static void +gtk_rb_node_rotate_left (GtkRbTree *tree, + GtkRbNode *node) +{ + GtkRbNode *right, *p; + + right = node->right; + p = parent (node); + + node->right = right->left; + if (right->left) + set_parent (tree, right->left, node); + + set_parent (tree, right, p); + if (p) + { + if (node == p->left) + p->left = right; + else + p->right = right; + } + + right->left = node; + set_parent (tree, node, right); + + gtk_rb_node_mark_dirty (node, FALSE); + gtk_rb_node_mark_dirty (right, FALSE); +} + +static void +gtk_rb_node_rotate_right (GtkRbTree *tree, + GtkRbNode *node) +{ + GtkRbNode *left, *p; + + left = node->left; + p = parent (node); + + node->left = left->right; + if (left->right) + set_parent (tree, left->right, node); + + set_parent (tree, left, p); + if (p) + { + if (node == p->right) + p->right = left; + else + p->left = left; + } + + /* link node and left */ + left->right = node; + set_parent (tree, node, left); + + gtk_rb_node_mark_dirty (node, FALSE); + gtk_rb_node_mark_dirty (left, FALSE); +} + +static gboolean +is_red (GtkRbNode *node_or_null) +{ + if (node_or_null == NULL) + return FALSE; + else + return node_or_null->red; +} + +static inline gboolean +is_black (GtkRbNode *node_or_null) +{ + return !is_red (node_or_null); +} + +static void +set_black (GtkRbNode *node_or_null) +{ + if (node_or_null == NULL) + return; + + node_or_null->red = FALSE; +} + +static void +set_red (GtkRbNode *node_or_null) +{ + if (node_or_null == NULL) + return; + + node_or_null->red = TRUE; +} + +static void +gtk_rb_tree_insert_fixup (GtkRbTree *tree, + GtkRbNode *node) +{ + GtkRbNode *p; + + /* check Red-Black properties */ + for (p = parent (node); + p && is_red (p); + p = parent (node)) + { + GtkRbNode *pp = parent (p); + + /* we have a violation */ + g_assert (pp); + + if (p == pp->left) + { + GtkRbNode *uncle = pp->right; + + if (is_red (uncle)) + { + /* uncle is red */ + set_black (p); + set_black (uncle); + set_red (pp); + node = pp; + } + else + { + /* uncle is black */ + if (node == p->right) + { + /* make node a left child */ + node = p; + gtk_rb_node_rotate_left (tree, node); + p = parent (node); + pp = parent (p); + } + /* recolor and rotate */ + set_black (p); + set_red (pp); + gtk_rb_node_rotate_right (tree, pp); + } + } + else + { + /* mirror image of above code */ + GtkRbNode *uncle = pp->left; + + if (is_red (uncle)) + { + /* uncle is red */ + set_black (p); + set_black (uncle); + set_red (pp); + node = pp; + } + else + { + /* uncle is black */ + if (node == p->left) + { + node = p; + gtk_rb_node_rotate_right (tree, node); + p = parent (node); + pp = parent (p); + } + set_black (p); + set_red (pp); + gtk_rb_node_rotate_left (tree, pp); + } + } + } + + set_black (tree->root); +} + +static void +gtk_rb_tree_remove_node_fixup (GtkRbTree *tree, + GtkRbNode *node, + GtkRbNode *p) +{ + while (node != tree->root && is_black (node)) + { + if (node == p->left) + { + GtkRbNode *w = p->right; + + if (is_red (w)) + { + set_black (w); + set_red (p); + gtk_rb_node_rotate_left (tree, p); + w = p->right; + } + if (is_black (w->left) && is_black (w->right)) + { + set_red (w); + node = p; + } + else + { + if (is_black (w->right)) + { + set_black (w->left); + set_red (w); + gtk_rb_node_rotate_right (tree, w); + w = p->right; + } + w->red = p->red; + set_black (p); + set_black (w->right); + gtk_rb_node_rotate_left (tree, p); + node = tree->root; + } + } + else + { + GtkRbNode *w = p->left; + if (is_red (w)) + { + set_black (w); + set_red (p); + gtk_rb_node_rotate_right (tree, p); + w = p->left; + } + if (is_black (w->right) && is_black (w->left)) + { + set_red (w); + node = p; + } + else + { + if (is_black (w->left)) + { + set_black (w->right); + set_red (w); + gtk_rb_node_rotate_left (tree, w); + w = p->left; + } + w->red = p->red; + set_black (p); + set_black (w->left); + gtk_rb_node_rotate_right (tree, p); + node = tree->root; + } + } + + p = parent (node); + } + + set_black (node); +} + +GtkRbTree * +gtk_rb_tree_new_for_size (gsize element_size, + gsize augment_size, + GtkRbTreeAugmentFunc augment_func, + GDestroyNotify clear_func, + GDestroyNotify clear_augment_func) +{ + GtkRbTree *tree; + + tree = g_slice_new0 (GtkRbTree); + tree->ref_count = 1; + + tree->element_size = element_size; + tree->augment_size = augment_size; + tree->augment_func = augment_func; + tree->clear_func = clear_func; + tree->clear_augment_func = clear_augment_func; + + return tree; +} + +GtkRbTree * +gtk_rb_tree_ref (GtkRbTree *tree) +{ + tree->ref_count++; + + return tree; +} + +void +gtk_rb_tree_unref (GtkRbTree *tree) +{ + tree->ref_count--; + if (tree->ref_count > 0) + return; + + if (tree->root) + gtk_rb_node_free_deep (tree, tree->root); + + g_slice_free (GtkRbTree, tree); +} + +gpointer +gtk_rb_tree_get_first (GtkRbTree *tree) +{ + if (tree->root == NULL) + return NULL; + + return NODE_TO_POINTER (gtk_rb_node_get_first (tree->root)); +} + +gpointer +gtk_rb_tree_get_last (GtkRbTree *tree) +{ + if (tree->root == NULL) + return NULL; + + return NODE_TO_POINTER (gtk_rb_node_get_last (tree->root)); +} + +gpointer +gtk_rb_tree_node_get_previous (gpointer node) +{ + return NODE_TO_POINTER (gtk_rb_node_get_previous (NODE_FROM_POINTER (node))); +} + +gpointer +gtk_rb_tree_node_get_next (gpointer node) +{ + return NODE_TO_POINTER (gtk_rb_node_get_next (NODE_FROM_POINTER (node))); +} + +gpointer +gtk_rb_tree_get_root (GtkRbTree *tree) +{ + return NODE_TO_POINTER (tree->root); +} + +gpointer +gtk_rb_tree_node_get_parent (gpointer node) +{ + return NODE_TO_POINTER (parent (NODE_FROM_POINTER (node))); +} + +gpointer +gtk_rb_tree_node_get_left (gpointer node) +{ + return NODE_TO_POINTER (NODE_FROM_POINTER (node)->left); +} + +gpointer +gtk_rb_tree_node_get_right (gpointer node) +{ + return NODE_TO_POINTER (NODE_FROM_POINTER (node)->right); +} + +gpointer +gtk_rb_tree_get_augment (GtkRbTree *tree, + gpointer node) +{ + GtkRbNode *rbnode = NODE_FROM_POINTER (node); + + gtk_rb_node_clean (tree, rbnode); + + return NODE_TO_AUG_POINTER (tree, rbnode); +} + +GtkRbTree * +gtk_rb_tree_node_get_tree (gpointer node) +{ + return tree (NODE_FROM_POINTER (node)); +} + +void +gtk_rb_tree_node_mark_dirty (gpointer node) +{ + gtk_rb_node_mark_dirty (NODE_FROM_POINTER (node), TRUE); +} + +gpointer +gtk_rb_tree_insert_before (GtkRbTree *tree, + gpointer node) +{ + GtkRbNode *result; + + + if (tree->root == NULL) + { +#ifdef DUMP_MODIFICATION + g_print ("add (tree, 0); /* 0x%p */\n", tree); +#endif /* DUMP_MODIFICATION */ + + g_assert (node == NULL); + + result = gtk_rb_node_new (tree); + tree->root = result; + } + else if (node == NULL) + { + return gtk_rb_tree_insert_after (tree, gtk_rb_tree_get_last (tree)); + } + else + { + GtkRbNode *current = NODE_FROM_POINTER (node); + +#ifdef DUMP_MODIFICATION + g_print ("add (tree, %u); /* 0x%p */\n", position (tree, current), tree); +#endif /* DUMP_MODIFICATION */ + + /* setup new node */ + result = gtk_rb_node_new (tree); + + if (current->left) + { + current = gtk_rb_node_get_last (current->left); + current->right = result; + } + else + { + current->left = result; + } + set_parent (tree, result, current); + gtk_rb_node_mark_dirty (current, TRUE); + } + + gtk_rb_tree_insert_fixup (tree, result); + + return NODE_TO_POINTER (result); +} + +gpointer +gtk_rb_tree_insert_after (GtkRbTree *tree, + gpointer node) +{ + GtkRbNode *current, *result; + + if (node == NULL) + return gtk_rb_tree_insert_before (tree, gtk_rb_tree_get_first (tree)); + + current = NODE_FROM_POINTER (node); + +#ifdef DUMP_MODIFICATION + g_print ("add (tree, %u); /* 0x%p */\n", position (tree, current) + 1, tree); +#endif /* DUMP_MODIFICATION */ + + /* setup new node */ + result = gtk_rb_node_new (tree); + + if (current->right) + { + current = gtk_rb_node_get_first (current->right); + current->left = result; + } + else + { + current->right = result; + } + set_parent (tree, result, current); + gtk_rb_node_mark_dirty (current, TRUE); + + gtk_rb_tree_insert_fixup (tree, result); + + return NODE_TO_POINTER (result); +} + +void +gtk_rb_tree_remove (GtkRbTree *tree, + gpointer node) +{ + GtkRbNode *x, *y, *p, *real_node; + + real_node = NODE_FROM_POINTER (node); + +#ifdef DUMP_MODIFICATION + g_print ("delete (tree, %u); /* 0x%p */\n", position (tree, real_node), tree); +#endif /* DUMP_MODIFICATION */ + + y = real_node; + if (y->left && y->right) + { + y = y->right; + + while (y->left) + y = y->left; + } + + /* x is y's only child, or nil */ + if (y->left) + x = y->left; + else + x = y->right; + + /* remove y from the parent chain */ + p = parent (y); + if (x != NULL) + set_parent (tree, x, p); + if (p) + { + if (y == p->left) + p->left = x; + else + p->right = x; + gtk_rb_node_mark_dirty (p, TRUE); + } + else + { + if (x == NULL) + tree->root = NULL; + } + + /* We need to clean up the validity of the tree. + */ + if (is_black (y)) + gtk_rb_tree_remove_node_fixup (tree, x, p); + + if (y != real_node) + { + /* Move the node over */ + if (is_red (real_node) != is_red (y)) + y->red = !y->red; + + y->left = real_node->left; + if (y->left) + set_parent (tree, y->left, y); + y->right = real_node->right; + if (y->right) + set_parent (tree, y->right, y); + p = parent (real_node); + set_parent (tree, y, p); + if (p) + { + if (p->left == real_node) + p->left = y; + else + p->right = y; + gtk_rb_node_mark_dirty (p, TRUE); + } + gtk_rb_node_mark_dirty (y, TRUE); + } + + gtk_rb_node_free (tree, real_node); +} + +void +gtk_rb_tree_remove_all (GtkRbTree *tree) +{ +#ifdef DUMP_MODIFICATION + g_print ("delete_all (tree); /* 0x%p */\n", tree); +#endif /* DUMP_MODIFICATION */ + + if (tree->root) + gtk_rb_node_free_deep (tree, tree->root); + + tree->root = NULL; +} + diff --git a/src/gtk-list-models/gtkrbtreeprivate.h b/src/gtk-list-models/gtkrbtreeprivate.h new file mode 100644 index 000000000..45aba5cc2 --- /dev/null +++ b/src/gtk-list-models/gtkrbtreeprivate.h @@ -0,0 +1,75 @@ +/* gtkrbtree.h + * Copyright (C) 2000 Red Hat, Inc., Jonathan Blandford + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library 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 + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library. If not, see . + */ + +/* A Red-Black Tree implementation used specifically by GtkTreeView. + */ +#ifndef __GTK_RB_TREE_H__ +#define __GTK_RB_TREE_H__ + +#include + + +G_BEGIN_DECLS + + +typedef struct _GtkRbTree GtkRbTree; + +typedef void (* GtkRbTreeAugmentFunc) (GtkRbTree *tree, + gpointer node_augment, + gpointer node, + gpointer left, + gpointer right); + +GtkRbTree * gtk_rb_tree_new_for_size (gsize element_size, + gsize augment_size, + GtkRbTreeAugmentFunc augment_func, + GDestroyNotify clear_func, + GDestroyNotify clear_augment_func); +#define gtk_rb_tree_new(type, augment_type, augment_func, clear_func, clear_augment_func) \ + gtk_rb_tree_new_for_size (sizeof (type), sizeof (augment_type), (augment_func), (clear_func), (clear_augment_func)) + +GtkRbTree * gtk_rb_tree_ref (GtkRbTree *tree); +void gtk_rb_tree_unref (GtkRbTree *tree); + +gpointer gtk_rb_tree_get_root (GtkRbTree *tree); +gpointer gtk_rb_tree_get_first (GtkRbTree *tree); +gpointer gtk_rb_tree_get_last (GtkRbTree *tree); + +gpointer gtk_rb_tree_node_get_previous (gpointer node); +gpointer gtk_rb_tree_node_get_next (gpointer node); +gpointer gtk_rb_tree_node_get_parent (gpointer node); +gpointer gtk_rb_tree_node_get_left (gpointer node); +gpointer gtk_rb_tree_node_get_right (gpointer node); +GtkRbTree * gtk_rb_tree_node_get_tree (gpointer node); +void gtk_rb_tree_node_mark_dirty (gpointer node); + +gpointer gtk_rb_tree_get_augment (GtkRbTree *tree, + gpointer node); + +gpointer gtk_rb_tree_insert_before (GtkRbTree *tree, + gpointer node); +gpointer gtk_rb_tree_insert_after (GtkRbTree *tree, + gpointer node); +void gtk_rb_tree_remove (GtkRbTree *tree, + gpointer node); +void gtk_rb_tree_remove_all (GtkRbTree *tree); + + +G_END_DECLS + + +#endif /* __GTK_RB_TREE_H__ */ diff --git a/src/gtk-list-models/gtksortlistmodel.c b/src/gtk-list-models/gtksortlistmodel.c new file mode 100644 index 000000000..7372934cb --- /dev/null +++ b/src/gtk-list-models/gtksortlistmodel.c @@ -0,0 +1,562 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#include "phosh-config.h" + +#include "gtksortlistmodel.h" + +#include + +/** + * SECTION:gtksortlistmodel + * @title: GtkSortListModel + * @short_description: A list model that sorts its items + * @see_also: #GListModel + * + * #GtkSortListModel is a list model that takes a list model and + * sorts its elements according to a compare function. + * + * #GtkSortListModel is a generic model and because of that it + * cannot take advantage of any external knowledge when sorting. + * If you run into performance issues with #GtkSortListModel, it + * is strongly recommended that you write your own sorting list + * model. + */ + +enum { + PROP_0, + PROP_HAS_SORT, + PROP_ITEM_TYPE, + PROP_MODEL, + NUM_PROPERTIES +}; + +struct _GtkSortListModel +{ + GObject parent_instance; + + GType item_type; + GListModel *model; + GCompareDataFunc sort_func; + gpointer user_data; + GDestroyNotify user_destroy; + + GSequence *sorted; /* NULL if sort_func == NULL */ + GSequence *unsorted; /* NULL if sort_func == NULL */ +}; + +struct _GtkSortListModelClass +{ + GObjectClass parent_class; +}; + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +static GType +gtk_sort_list_model_get_item_type (GListModel *list) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (list); + + return self->item_type; +} + +static guint +gtk_sort_list_model_get_n_items (GListModel *list) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (list); + + if (self->model == NULL) + return 0; + + if (self->sorted) + return g_sequence_get_length (self->sorted); + + return g_list_model_get_n_items (self->model); +} + +static gpointer +gtk_sort_list_model_get_item (GListModel *list, + guint position) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (list); + GSequenceIter *iter; + + if (self->model == NULL) + return NULL; + + if (self->unsorted == NULL) + return g_list_model_get_item (self->model, position); + + iter = g_sequence_get_iter_at_pos (self->sorted, position); + if (g_sequence_iter_is_end (iter)) + return NULL; + + return g_object_ref (g_sequence_get (iter)); +} + +static void +gtk_sort_list_model_model_init (GListModelInterface *iface) +{ + iface->get_item_type = gtk_sort_list_model_get_item_type; + iface->get_n_items = gtk_sort_list_model_get_n_items; + iface->get_item = gtk_sort_list_model_get_item; +} + +G_DEFINE_TYPE_WITH_CODE (GtkSortListModel, gtk_sort_list_model, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, gtk_sort_list_model_model_init)) + +static void +gtk_sort_list_model_remove_items (GtkSortListModel *self, + guint position, + guint n_items, + guint *unmodified_start, + guint *unmodified_end) +{ + GSequenceIter *unsorted_iter; + guint i, pos, start, end, length_before; + + start = end = length_before = g_sequence_get_length (self->sorted); + unsorted_iter = g_sequence_get_iter_at_pos (self->unsorted, position); + + for (i = 0; i < n_items ; i++) + { + GSequenceIter *sorted_iter; + GSequenceIter *next; + + next = g_sequence_iter_next (unsorted_iter); + + sorted_iter = g_sequence_get (unsorted_iter); + pos = g_sequence_iter_get_position (sorted_iter); + start = MIN (start, pos); + end = MIN (end, length_before - i - 1 - pos); + + g_sequence_remove (sorted_iter); + g_sequence_remove (unsorted_iter); + + unsorted_iter = next; + } + + *unmodified_start = start; + *unmodified_end = end; +} + +static void +gtk_sort_list_model_add_items (GtkSortListModel *self, + guint position, + guint n_items, + guint *unmodified_start, + guint *unmodified_end) +{ + GSequenceIter *unsorted_iter, *sorted_iter; + guint i, pos, start, end, length_before; + + unsorted_iter = g_sequence_get_iter_at_pos (self->unsorted, position); + start = end = length_before = g_sequence_get_length (self->sorted); + + for (i = 0; i < n_items; i++) + { + gpointer item = g_list_model_get_item (self->model, position + i); + sorted_iter = g_sequence_insert_sorted (self->sorted, item, self->sort_func, self->user_data); + g_sequence_insert_before (unsorted_iter, sorted_iter); + if (unmodified_start != NULL || unmodified_end != NULL) + { + pos = g_sequence_iter_get_position (sorted_iter); + start = MIN (start, pos); + end = MIN (end, length_before + i - pos); + } + } + + if (unmodified_start) + *unmodified_start = start; + if (unmodified_end) + *unmodified_end = end; +} + +static void +gtk_sort_list_model_items_changed_cb (GListModel *model, + guint position, + guint removed, + guint added, + GtkSortListModel *self) +{ + guint n_items, start, end, start2, end2; + + if (removed == 0 && added == 0) + return; + + if (self->sorted == NULL) + { + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + return; + } + + gtk_sort_list_model_remove_items (self, position, removed, &start, &end); + gtk_sort_list_model_add_items (self, position, added, &start2, &end2); + start = MIN (start, start2); + end = MIN (end, end2); + + n_items = g_sequence_get_length (self->sorted) - start - end; + g_list_model_items_changed (G_LIST_MODEL (self), start, n_items - added + removed, n_items); +} + +static void +gtk_sort_list_model_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_ITEM_TYPE: + self->item_type = g_value_get_gtype (value); + break; + + case PROP_MODEL: + gtk_sort_list_model_set_model (self, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_sort_list_model_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (object); + + switch (prop_id) + { + case PROP_HAS_SORT: + g_value_set_boolean (value, self->sort_func != NULL); + break; + + case PROP_ITEM_TYPE: + g_value_set_gtype (value, self->item_type); + break; + + case PROP_MODEL: + g_value_set_object (value, self->model); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gtk_sort_list_model_clear_model (GtkSortListModel *self) +{ + if (self->model == NULL) + return; + + g_signal_handlers_disconnect_by_func (self->model, gtk_sort_list_model_items_changed_cb, self); + g_clear_object (&self->model); + g_clear_pointer (&self->sorted, g_sequence_free); + g_clear_pointer (&self->unsorted, g_sequence_free); +} + +static void +gtk_sort_list_model_dispose (GObject *object) +{ + GtkSortListModel *self = GTK_SORT_LIST_MODEL (object); + + gtk_sort_list_model_clear_model (self); + if (self->user_destroy) + self->user_destroy (self->user_data); + self->sort_func = NULL; + self->user_data = NULL; + self->user_destroy = NULL; + + G_OBJECT_CLASS (gtk_sort_list_model_parent_class)->dispose (object); +}; + +static void +gtk_sort_list_model_class_init (GtkSortListModelClass *class) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (class); + + gobject_class->set_property = gtk_sort_list_model_set_property; + gobject_class->get_property = gtk_sort_list_model_get_property; + gobject_class->dispose = gtk_sort_list_model_dispose; + + /** + * GtkSortListModel:has-sort: + * + * If a sort function is set for this model + */ + properties[PROP_HAS_SORT] = + g_param_spec_boolean ("has-sort", + N_("has sort"), + N_("If a sort function is set for this model"), + FALSE, + G_PARAM_STATIC_STRINGS | G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkSortListModel:item-type: + * + * The #GType for items of this model + */ + properties[PROP_ITEM_TYPE] = + g_param_spec_gtype ("item-type", + N_("Item type"), + N_("The type of items of this list"), + G_TYPE_OBJECT, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + /** + * GtkSortListModel:model: + * + * The model being sorted + */ + properties[PROP_MODEL] = + g_param_spec_object ("model", + N_("Model"), + N_("The model being sorted"), + G_TYPE_LIST_MODEL, + G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, NUM_PROPERTIES, properties); +} + +static void +gtk_sort_list_model_init (GtkSortListModel *self) +{ +} + + +/** + * gtk_sort_list_model_new: + * @model: the model to sort + * @sort_func: (allow-none): sort function or %NULL to not sort items + * @user_data: user data passed to @sort_func + * @user_destroy: destroy notifier for @user_data + * + * Creates a new sort list model that uses the @sort_func to sort @model. + * + * Returns: a new #GtkSortListModel + **/ +GtkSortListModel * +gtk_sort_list_model_new (GListModel *model, + GCompareDataFunc sort_func, + gpointer user_data, + GDestroyNotify user_destroy) +{ + GtkSortListModel *result; + + g_return_val_if_fail (G_IS_LIST_MODEL (model), NULL); + + result = g_object_new (GTK_TYPE_SORT_LIST_MODEL, + "item-type", g_list_model_get_item_type (model), + "model", model, + NULL); + + if (sort_func) + gtk_sort_list_model_set_sort_func (result, sort_func, user_data, user_destroy); + + return result; +} + +/** + * gtk_sort_list_model_new_for_type: + * @item_type: the type of the items that will be returned + * + * Creates a new empty sort list model set up to return items of type @item_type. + * It is up to the application to set a proper sort function and model to ensure + * the item type is matched. + * + * Returns: a new #GtkSortListModel + **/ +GtkSortListModel * +gtk_sort_list_model_new_for_type (GType item_type) +{ + g_return_val_if_fail (g_type_is_a (item_type, G_TYPE_OBJECT), NULL); + + return g_object_new (GTK_TYPE_SORT_LIST_MODEL, + "item-type", item_type, + NULL); +} + +static void +gtk_sort_list_model_create_sequences (GtkSortListModel *self) +{ + if (!self->sort_func || self->model == NULL) + return; + + self->sorted = g_sequence_new (g_object_unref); + self->unsorted = g_sequence_new (NULL); + + gtk_sort_list_model_add_items (self, 0, g_list_model_get_n_items (self->model), NULL, NULL); +} + +/** + * gtk_sort_list_model_set_sort_func: + * @self: a #GtkSortListModel + * @sort_func: (allow-none): sort function or %NULL to not sort items + * @user_data: user data passed to @sort_func + * @user_destroy: destroy notifier for @user_data + * + * Sets the function used to sort items. The function will be called for every + * item and must return an integer less than, equal to, or greater than zero if + * for two items from the model if the first item is considered to be respectively + * less than, equal to, or greater than the second. + **/ +void +gtk_sort_list_model_set_sort_func (GtkSortListModel *self, + GCompareDataFunc sort_func, + gpointer user_data, + GDestroyNotify user_destroy) +{ + guint n_items; + + g_return_if_fail (GTK_IS_SORT_LIST_MODEL (self)); + g_return_if_fail (sort_func != NULL || (user_data == NULL && !user_destroy)); + + if (!sort_func && !self->sort_func) + return; + + if (self->user_destroy) + self->user_destroy (self->user_data); + + g_clear_pointer (&self->unsorted, g_sequence_free); + g_clear_pointer (&self->sorted, g_sequence_free); + self->sort_func = sort_func; + self->user_data = user_data; + self->user_destroy = user_destroy; + + gtk_sort_list_model_create_sequences (self); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (self)); + if (n_items > 1) + g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, n_items); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_HAS_SORT]); +} + +/** + * gtk_sort_list_model_set_model: + * @self: a #GtkSortListModel + * @model: (allow-none): The model to be sorted + * + * Sets the model to be sorted. The @model's item type must conform to + * the item type of @self. + **/ +void +gtk_sort_list_model_set_model (GtkSortListModel *self, + GListModel *model) +{ + guint removed, added; + + g_return_if_fail (GTK_IS_SORT_LIST_MODEL (self)); + g_return_if_fail (model == NULL || G_IS_LIST_MODEL (model)); + if (model) + { + g_return_if_fail (g_type_is_a (g_list_model_get_item_type (model), self->item_type)); + } + + if (self->model == model) + return; + + removed = g_list_model_get_n_items (G_LIST_MODEL (self)); + gtk_sort_list_model_clear_model (self); + + if (model) + { + self->model = g_object_ref (model); + g_signal_connect (model, "items-changed", G_CALLBACK (gtk_sort_list_model_items_changed_cb), self); + added = g_list_model_get_n_items (model); + + gtk_sort_list_model_create_sequences (self); + } + else + added = 0; + + if (removed > 0 || added > 0) + g_list_model_items_changed (G_LIST_MODEL (self), 0, removed, added); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODEL]); +} + +/** + * gtk_sort_list_model_get_model: + * @self: a #GtkSortListModel + * + * Gets the model currently sorted or %NULL if none. + * + * Returns: (nullable) (transfer none): The model that gets sorted + **/ +GListModel * +gtk_sort_list_model_get_model (GtkSortListModel *self) +{ + g_return_val_if_fail (GTK_IS_SORT_LIST_MODEL (self), NULL); + + return self->model; +} + +/** + * gtk_sort_list_model_has_sort: + * @self: a #GtkSortListModel + * + * Checks if a sort function is currently set on @self + * + * Returns: %TRUE if a sort function is set + **/ +gboolean +gtk_sort_list_model_has_sort (GtkSortListModel *self) +{ + g_return_val_if_fail (GTK_IS_SORT_LIST_MODEL (self), FALSE); + + return self->sort_func != NULL; +} + +/** + * gtk_sort_list_model_resort: + * @self: a #GtkSortListModel + * + * Causes @self to resort all items in the model. + * + * Calling this function is necessary when data used by the sort + * function has changed. + **/ +void +gtk_sort_list_model_resort (GtkSortListModel *self) +{ + guint n_items; + + g_return_if_fail (GTK_IS_SORT_LIST_MODEL (self)); + + if (self->sorted == NULL) + return; + + n_items = g_list_model_get_n_items (self->model); + if (n_items <= 1) + return; + + g_sequence_sort (self->sorted, self->sort_func, self->user_data); + + g_list_model_items_changed (G_LIST_MODEL (self), 0, n_items, n_items); +} + diff --git a/src/gtk-list-models/gtksortlistmodel.h b/src/gtk-list-models/gtksortlistmodel.h new file mode 100644 index 000000000..c1da26842 --- /dev/null +++ b/src/gtk-list-models/gtksortlistmodel.h @@ -0,0 +1,61 @@ +/* + * Copyright © 2018 Benjamin Otte + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + * + * Authors: Benjamin Otte + */ + +#ifndef __GTK_SORT_LIST_MODEL_H__ +#define __GTK_SORT_LIST_MODEL_H__ + + +#include +#include + + +G_BEGIN_DECLS + +#define GTK_TYPE_SORT_LIST_MODEL (gtk_sort_list_model_get_type ()) + +GDK_AVAILABLE_IN_ALL +G_DECLARE_FINAL_TYPE (GtkSortListModel, gtk_sort_list_model, GTK, SORT_LIST_MODEL, GObject) + +GDK_AVAILABLE_IN_ALL +GtkSortListModel * gtk_sort_list_model_new (GListModel *model, + GCompareDataFunc sort_func, + gpointer user_data, + GDestroyNotify user_destroy); +GDK_AVAILABLE_IN_ALL +GtkSortListModel * gtk_sort_list_model_new_for_type (GType item_type); + +GDK_AVAILABLE_IN_ALL +void gtk_sort_list_model_set_sort_func (GtkSortListModel *self, + GCompareDataFunc sort_func, + gpointer user_data, + GDestroyNotify user_destroy); +GDK_AVAILABLE_IN_ALL +gboolean gtk_sort_list_model_has_sort (GtkSortListModel *self); +GDK_AVAILABLE_IN_ALL +void gtk_sort_list_model_set_model (GtkSortListModel *self, + GListModel *model); +GDK_AVAILABLE_IN_ALL +GListModel * gtk_sort_list_model_get_model (GtkSortListModel *self); + +GDK_AVAILABLE_IN_ALL +void gtk_sort_list_model_resort (GtkSortListModel *self); + +G_END_DECLS + +#endif /* __GTK_SORT_LIST_MODEL_H__ */ diff --git a/src/gtk-list-models/meson.build b/src/gtk-list-models/meson.build new file mode 100644 index 000000000..6d0212cac --- /dev/null +++ b/src/gtk-list-models/meson.build @@ -0,0 +1,10 @@ +phosh_gtk_list_models_inc = include_directories('.') + +phosh_gtk_list_models_sources = files( + 'gtkfilterlistmodel.c', + 'gtkfilterlistmodel.h', + 'gtkrbtree.c', + 'gtkrbtreeprivate.h', + 'gtksortlistmodel.c', + 'gtksortlistmodel.h', +) diff --git a/src/gtk-mount-manager.c b/src/gtk-mount-manager.c new file mode 100644 index 000000000..4930f5b01 --- /dev/null +++ b/src/gtk-mount-manager.c @@ -0,0 +1,414 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-gtk-mount-manager" + +#include "gtk-mount-manager.h" +#include "gtk-mount-prompt.h" +#include "shell-priv.h" +#include "util.h" + +#include + +/** + * PhoshGtkMountManager: + * + * Provides the org.Gtk.GtkMountOperationHandler DBus interface + * + * The interface is responsible to handle the ui parts of a #GtkMountOperation. + */ + +#define GTK_MOUNT_DBUS_NAME "org.gtk.MountOperationHandler" + +enum { + NEW_PROMPT, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +static void phosh_gtk_mount_manager_gtk_mount_iface_init ( + PhoshDBusMountOperationHandlerIface *iface); + +typedef struct _PhoshGtkMountManager { + PhoshDBusMountOperationHandlerSkeleton parent; + + int dbus_name_id; + PhoshGtkMountPrompt *prompt; + char *object_id; + + GDBusMethodInvocation *invocation; +} PhoshGtkMountManager; + +G_DEFINE_TYPE_WITH_CODE (PhoshGtkMountManager, + phosh_gtk_mount_manager, + PHOSH_DBUS_TYPE_MOUNT_OPERATION_HANDLER_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_MOUNT_OPERATION_HANDLER, + phosh_gtk_mount_manager_gtk_mount_iface_init)); + + +static void +on_prompt_done (PhoshGtkMountManager *self, PhoshGtkMountPrompt *prompt) +{ + gboolean cancelled; + GMountOperationResult response = G_MOUNT_OPERATION_ABORTED; + g_autoptr (GVariantDict) response_details = g_variant_dict_new (NULL); + + g_return_if_fail (PHOSH_IS_GTK_MOUNT_MANAGER (self)); + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (prompt)); + + cancelled = phosh_gtk_mount_prompt_get_cancelled (prompt); + g_debug ("Prompt done, cancelled: %d", cancelled); + + if (phosh_gtk_mount_prompt_get_choices (prompt)) { /* AskQuestion / ShowProcesses */ + int choice = phosh_gtk_mount_prompt_get_choice (prompt); + + if (!cancelled) { + response = G_MOUNT_OPERATION_HANDLED; + g_variant_dict_insert (response_details, "choice", "i", choice); + } + } else { /* AskPassword / Default buttons */ + if (!cancelled) { + const char *password; + + response = G_MOUNT_OPERATION_HANDLED; + password = phosh_gtk_mount_prompt_get_password (prompt); + g_variant_dict_insert (response_details, "password", "s", password ?: "", NULL); + /* + * GTKs gtkmountoperation:call_password_proxy_cb only cares about + * 'password', 'password_save', 'hidden_volume', 'system_volume' and 'pim' + * so no need to bother with usernames and domains here + */ + /* TODO: 'password_save' */ + } + } + + if (self->invocation) { + /* + * phosh_dbus_mount_operation_handler_complete_ask_question has the same + * signature and params so we can use either one + */ + phosh_dbus_mount_operation_handler_complete_ask_password ( + PHOSH_DBUS_MOUNT_OPERATION_HANDLER (self), + self->invocation, response, + g_variant_dict_end (response_details)); + self->invocation = NULL; + } else { + g_warning ("No invocation!"); + } +} + + +static void +end_ask_invocation (PhoshGtkMountManager *self) +{ + g_autoptr (GVariantDict) response_details = g_variant_dict_new (NULL); + + if (!self->invocation) + return; + + /* + * phosh_dbus_mount_operation_handler_complete_* all have the same + * signature and params so we can any of them: + */ + phosh_dbus_mount_operation_handler_complete_ask_password ( + PHOSH_DBUS_MOUNT_OPERATION_HANDLER (self), + self->invocation, G_MOUNT_OPERATION_UNHANDLED, + g_variant_dict_end (response_details)); + self->invocation = NULL; +} + + +static void +new_prompt (PhoshGtkMountManager *self, + const char *message, + const char *icon_name, + const char *default_user, + const char *default_domain, + GVariant *pids, + const char *const *choices, + GAskPasswordFlags ask_flags) +{ + g_debug ("New prompt for '%s'", message); + + g_clear_pointer ((PhoshSystemModalDialog**)&self->prompt, phosh_system_modal_dialog_close); + + self->prompt = PHOSH_GTK_MOUNT_PROMPT (phosh_gtk_mount_prompt_new ( + message, + icon_name, + default_user, + default_domain, + pids, + choices, + ask_flags)); + g_signal_connect_swapped (self->prompt, + "closed", + G_CALLBACK (on_prompt_done), + self); + + gtk_widget_set_visible (GTK_WIDGET (self->prompt), TRUE); + + g_signal_emit (self, signals[NEW_PROMPT], 0); +} + + +static gboolean +handle_ask_password (PhoshDBusMountOperationHandler *object, + GDBusMethodInvocation *invocation, + const char *arg_object_id, + const char *arg_message, + const char *arg_icon_name, + const char *arg_default_user, + const char *arg_default_domain, + guint arg_flags) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + g_debug ("DBus call AskPassword for '%s'", arg_object_id); + + if (self->invocation) + end_ask_invocation (self); + self->invocation = invocation; + + new_prompt (self, + arg_message, + arg_icon_name, + arg_default_user, + arg_default_domain, + NULL, + NULL, + arg_flags); + + return TRUE; +} + +static gboolean +handle_ask_question ( PhoshDBusMountOperationHandler *object, + GDBusMethodInvocation *invocation, + const char *arg_object_id, + const char *arg_message, + const char *arg_icon_name, + const char *const *arg_choices) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + g_debug ("DBus call AskQuestion: %s", arg_object_id); + + /* Cancel an ongoing dialog */ + if (self->invocation) + end_ask_invocation (self); + self->invocation = invocation; + + new_prompt (self, + arg_message, + arg_icon_name, + NULL, + NULL, + NULL, + arg_choices, + 0); + + return TRUE; +} + +static gboolean +handle_show_processes (PhoshDBusMountOperationHandler *object, + GDBusMethodInvocation *invocation, + const char *arg_object_id, + const char *arg_message, + const char *arg_icon_name, + GVariant *arg_application_pids, + const char *const *arg_choices) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + g_debug ("DBus call ShowProcesses: %s", arg_object_id); + + if (arg_object_id == NULL) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No object id"); + invocation = NULL; + return TRUE; + } + + if (!g_strcmp0 (arg_object_id, self->object_id) && self->prompt) { + g_debug ("Updating dialog %s", self->object_id); + + phosh_gtk_mount_prompt_set_pids (self->prompt, arg_application_pids); + self->invocation = invocation; + return TRUE; + } + + g_clear_pointer (&self->object_id, g_free); + self->object_id = g_strdup (arg_object_id); + + if (self->invocation) + end_ask_invocation (self); + self->invocation = invocation; + + new_prompt (self, + arg_message, + arg_icon_name, + NULL, + NULL, + arg_application_pids, + arg_choices, + 0); + + return TRUE; +} + +static gboolean +handle_close (PhoshDBusMountOperationHandler *object, + GDBusMethodInvocation *invocation) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + g_debug ("DBus call Close"); + + end_ask_invocation (self); + + g_clear_pointer ((PhoshSystemModalDialog**)&self->prompt, phosh_system_modal_dialog_close); + g_clear_pointer (&self->object_id, g_free); + + phosh_dbus_mount_operation_handler_complete_close ( + object, invocation); + + return TRUE; +} + +static void +phosh_gtk_mount_manager_gtk_mount_iface_init (PhoshDBusMountOperationHandlerIface *iface) +{ + iface->handle_ask_password = handle_ask_password; + iface->handle_ask_question = handle_ask_question; + iface->handle_close = handle_close; + iface->handle_show_processes = handle_show_processes; +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (user_data); + + g_debug ("Acquired name %s", name); + g_return_if_fail (PHOSH_IS_GTK_MOUNT_MANAGER (self)); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshGtkMountManager *self = user_data; + + g_autoptr (GError) err = NULL; + + if (g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/gtk/MountOperationHandler", + &err)) { + g_debug ("Mount operation handler exported"); + } else { + g_warning ("Failed to export on %s: %s", GTK_MOUNT_DBUS_NAME, err->message); + } +} + + +static void +phosh_gtk_mount_manager_dispose (GObject *object) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + end_ask_invocation (self); + g_clear_pointer (&self->prompt, phosh_cp_widget_destroy); + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + G_OBJECT_CLASS (phosh_gtk_mount_manager_parent_class)->dispose (object); +} + + +static void +phosh_gtk_mount_manager_finalize (GObject *object) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + g_clear_pointer (&self->object_id, g_free); + + G_OBJECT_CLASS (phosh_gtk_mount_manager_parent_class)->finalize (object); +} + + +static void +phosh_gtk_mount_manager_constructed (GObject *object) +{ + PhoshGtkMountManager *self = PHOSH_GTK_MOUNT_MANAGER (object); + + G_OBJECT_CLASS (phosh_gtk_mount_manager_parent_class)->constructed (object); + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + GTK_MOUNT_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); +} + + +static void +phosh_gtk_mount_manager_class_init (PhoshGtkMountManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_gtk_mount_manager_constructed; + object_class->dispose = phosh_gtk_mount_manager_dispose; + object_class->finalize = phosh_gtk_mount_manager_finalize; + + /** + * PhoshGtkMountManager::new-prompt + * @self: The #PhoshGtkMountManager emitting this signal + * + * Emitted when a new prompt is shown + */ + signals[NEW_PROMPT] = g_signal_new ( + "new-prompt", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_gtk_mount_manager_init (PhoshGtkMountManager *self) +{ +} + + +PhoshGtkMountManager * +phosh_gtk_mount_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_GTK_MOUNT_MANAGER, NULL); +} diff --git a/src/gtk-mount-manager.h b/src/gtk-mount-manager.h new file mode 100644 index 000000000..b9b5c3d16 --- /dev/null +++ b/src/gtk-mount-manager.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "dbus/phosh-gtk-mountoperation-dbus.h" +#include + +#define PHOSH_TYPE_GTK_MOUNT_MANAGER (phosh_gtk_mount_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshGtkMountManager, phosh_gtk_mount_manager, PHOSH, GTK_MOUNT_MANAGER, + PhoshDBusMountOperationHandlerSkeleton) + +PhoshGtkMountManager *phosh_gtk_mount_manager_new (void); diff --git a/src/gtk-mount-prompt.c b/src/gtk-mount-prompt.c new file mode 100644 index 000000000..34bb58e48 --- /dev/null +++ b/src/gtk-mount-prompt.c @@ -0,0 +1,553 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-gtk-mount-prompt" + +#include "phosh-config.h" + +#include "gtk-mount-prompt.h" + +#include + +/** + * PhoshGtkMountPrompt: + * + * A modal prompt for #PhoshGtkMountManager + * + * The #PhoshGtkMountPrompt is used to query the needed information + * for the #PhoshGtkMountManager. + */ + +enum { + PROP_0, + PROP_MESSAGE, + PROP_ICON_NAME, + PROP_DEFAULT_USER, + PROP_DEFAULT_DOMAIN, + PROP_PIDS, + PROP_CHOICES, + PROP_ASK_FLAGS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +struct _PhoshGtkMountPrompt { + PhoshSystemModalDialog parent; + + GtkWidget *lbl_msg; + GtkWidget *lbl_password; + GtkWidget *img_icon; + GtkWidget *lbl_user; + GtkWidget *entry_user; + GtkWidget *lbl_domain; + GtkWidget *entry_domain; + GtkWidget *entry_password; + GtkEntryBuffer *password_buffer; + gboolean pw_visible; + + char *message; + char *icon_name; + char *default_user; + char *default_domain; + GStrv choices; + int choice; + GVariant *pids; + GAskPasswordFlags ask_flags; + + gboolean cancelled; +}; +G_DEFINE_TYPE (PhoshGtkMountPrompt, phosh_gtk_mount_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + + +static void G_GNUC_NULL_TERMINATED +set_visible (gboolean visible, ...) +{ + va_list args; + GtkWidget *widget; + + va_start (args, visible); + do { + widget = va_arg (args, gpointer); + if (widget == NULL) + break; + gtk_widget_set_visible (widget, visible); + } while (TRUE); + va_end (args); +} + + +static void +set_entries_visible (PhoshGtkMountPrompt *self, gboolean visible) +{ + set_visible (visible, self->lbl_password, self->entry_password, NULL); + set_visible (visible, self->lbl_user, self->entry_user, NULL); + set_visible (visible, self->lbl_domain, self->entry_domain, NULL); +} + + +static void +set_message (PhoshGtkMountPrompt *self, const char *message) +{ + g_autofree char *primary = NULL; + char *secondary = NULL; + + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (!g_strcmp0 (self->message, message)) + return; + + g_clear_pointer (&self->message, g_free); + self->message = g_strdup (message); + + primary = strstr (message, "\n"); + if (primary) { + secondary = primary + 1; + primary = g_strndup (message, primary - message); + } + + phosh_system_modal_dialog_set_title (PHOSH_SYSTEM_MODAL_DIALOG (self), primary ?: message); + if (secondary) { + gtk_label_set_label (GTK_LABEL (self->lbl_msg), secondary); + gtk_widget_set_visible (self->lbl_msg, TRUE); + } else { + gtk_widget_set_visible (self->lbl_msg, FALSE); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MESSAGE]); +} + + +static void +set_icon_name (PhoshGtkMountPrompt *self, const char *icon_name) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (!g_strcmp0 (self->icon_name, icon_name)) + return; + + g_clear_pointer (&self->icon_name, g_free); + self->icon_name = g_strdup (icon_name); + + gtk_image_set_from_icon_name (GTK_IMAGE (self->img_icon), + (icon_name && strlen (icon_name)) ? + self->icon_name : "dialog-password", + -1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +static void +set_default_user (PhoshGtkMountPrompt *self, const char *default_user) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (!g_strcmp0 (self->default_user, default_user)) + return; + + g_clear_pointer (&self->default_user, g_free); + self->default_user = g_strdup (default_user); + + gtk_entry_set_text (GTK_ENTRY (self->entry_domain), self->default_user); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_USER]); +} + + +static void +set_default_domain (PhoshGtkMountPrompt *self, const char *default_domain) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (!g_strcmp0 (self->default_domain, default_domain)) + return; + + g_clear_pointer (&self->default_domain, g_free); + self->default_domain = g_strdup (default_domain); + + gtk_entry_set_text (GTK_ENTRY (self->entry_domain), self->default_domain); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_DOMAIN]); +} + + +static void +on_button_clicked (PhoshGtkMountPrompt *self, GtkButton *btn) +{ + int num; + + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + g_return_if_fail (GTK_IS_BUTTON (btn)); + + num = GPOINTER_TO_INT (g_object_get_data (G_OBJECT (btn), "phosh-num")); + self->choice = num; + + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +set_choices (PhoshGtkMountPrompt *self, GStrv choices) +{ + g_autoptr (GList) buttons = NULL; + + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (choices == NULL) + return; + + g_clear_pointer (&self->choices, g_strfreev); + self->choices = g_strdupv (choices); + buttons = phosh_system_modal_dialog_get_buttons (PHOSH_SYSTEM_MODAL_DIALOG (self)); + for (GList *elem = buttons; elem; elem = elem->next) + gtk_widget_destroy (GTK_WIDGET (elem->data)); + + for (int i = 0; i < g_strv_length (self->choices); i++) { + GtkWidget *btn = gtk_button_new_with_label (self->choices[i]); + g_object_set_data (G_OBJECT (btn), "phosh-num", GINT_TO_POINTER (i)); + gtk_widget_set_visible (GTK_WIDGET (btn), TRUE); + phosh_system_modal_dialog_add_button (PHOSH_SYSTEM_MODAL_DIALOG (self), btn, -1); + g_signal_connect_swapped (btn, "clicked", G_CALLBACK (on_button_clicked), self); + + if (i == 0) { + GtkStyleContext *context = gtk_widget_get_style_context (btn); + gtk_style_context_add_class (context, "suggested-action"); + gtk_widget_grab_focus (btn); + } + } + set_entries_visible (self, FALSE); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHOICES]); +} + + +static void +set_ask_flags (PhoshGtkMountPrompt *self, GAskPasswordFlags flags) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (self->ask_flags == flags) + return; + + self->ask_flags = flags; + g_debug ("Flags 0x%.2x", flags); + + if (flags & G_ASK_PASSWORD_SAVING_SUPPORTED) { + /* TODO */ + } + + set_visible (!!(flags & G_ASK_PASSWORD_NEED_PASSWORD), + self->lbl_password, self->entry_password, NULL); + + set_visible (!!(flags & G_ASK_PASSWORD_NEED_USERNAME), + self->lbl_user, self->entry_user, NULL); + + set_visible (!!(flags & G_ASK_PASSWORD_NEED_DOMAIN), + self->lbl_domain, self->entry_domain, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ASK_FLAGS]); +} + + +static void +phosh_gtk_mount_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshGtkMountPrompt *self = PHOSH_GTK_MOUNT_PROMPT (obj); + + switch (prop_id) { + case PROP_MESSAGE: + set_message (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + set_icon_name (self, g_value_get_string (value)); + break; + case PROP_DEFAULT_USER: + set_default_user (self, g_value_get_string (value)); + break; + case PROP_DEFAULT_DOMAIN: + set_default_domain (self, g_value_get_string (value)); + break; + case PROP_CHOICES: + set_choices (self, g_value_get_boxed (value)); + break; + case PROP_PIDS: + phosh_gtk_mount_prompt_set_pids (self, g_value_get_variant (value)); + break; + case PROP_ASK_FLAGS: + set_ask_flags (self, g_value_get_flags (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_gtk_mount_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshGtkMountPrompt *self = PHOSH_GTK_MOUNT_PROMPT (obj); + + switch (prop_id) { + case PROP_MESSAGE: + g_value_set_string (value, self->message ?: ""); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name ?: ""); + break; + case PROP_DEFAULT_USER: + g_value_set_string (value, self->default_user ?: ""); + break; + case PROP_DEFAULT_DOMAIN: + g_value_set_string (value, self->default_domain ?: ""); + break; + case PROP_CHOICES: + g_value_set_boxed (value, self->choices); + break; + case PROP_PIDS: + g_value_set_variant (value, self->pids); + break; + case PROP_ASK_FLAGS: + g_value_set_enum (value, self->ask_flags); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +on_connect_clicked (PhoshGtkMountPrompt *self) +{ + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +on_dialog_canceled (PhoshGtkMountPrompt *self) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + self->cancelled = TRUE; + + g_signal_emit (self, signals[CLOSED], 0); +} + + +static void +phosh_gtk_mount_prompt_finalize (GObject *obj) +{ + PhoshGtkMountPrompt *self = PHOSH_GTK_MOUNT_PROMPT (obj); + + g_free (self->message); + g_free (self->icon_name); + g_free (self->default_user); + g_free (self->default_domain); + g_clear_pointer (&self->choices, g_strfreev); + g_clear_pointer (&self->pids, g_variant_unref); + + G_OBJECT_CLASS (phosh_gtk_mount_prompt_parent_class)->finalize (obj); +} + + +static void +phosh_gtk_mount_prompt_class_init (PhoshGtkMountPromptClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_gtk_mount_prompt_get_property; + object_class->set_property = phosh_gtk_mount_prompt_set_property; + object_class->finalize = phosh_gtk_mount_prompt_finalize; + + props[PROP_MESSAGE] = + g_param_spec_string ("message", + "", + "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "", + "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + props[PROP_DEFAULT_USER] = + g_param_spec_string ("default-user", + "", + "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + props[PROP_DEFAULT_DOMAIN] = + g_param_spec_string ("default-domain", + "", + "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + props[PROP_CHOICES] = + g_param_spec_boxed ("choices", + "", + "", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + props[PROP_PIDS] = + g_param_spec_variant ("pids", + "", + "", + G_VARIANT_TYPE_ARRAY, + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + props[PROP_ASK_FLAGS] = + g_param_spec_flags ("ask-flags", + "", + "", + G_TYPE_ASK_PASSWORD_FLAGS, + 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/gtk-mount-prompt.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, lbl_msg); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, lbl_password); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, lbl_user); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, lbl_domain); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, img_icon); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, entry_password); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, entry_user); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, entry_domain); + gtk_widget_class_bind_template_child (widget_class, PhoshGtkMountPrompt, password_buffer); + gtk_widget_class_bind_template_callback (widget_class, on_connect_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); + + gtk_widget_class_set_css_name (widget_class, "phosh-gtk-mount-prompt"); +} + + +static void +phosh_gtk_mount_prompt_init (PhoshGtkMountPrompt *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_gtk_mount_prompt_new (const char *message, + const char *icon_name, + const char *default_user, + const char *default_domain, + GVariant *pids, + const char *const *choices, + GAskPasswordFlags ask_flags) +{ + return g_object_new (PHOSH_TYPE_GTK_MOUNT_PROMPT, + "message", message, + "icon-name", icon_name, + "ask-flags", ask_flags, + "default-user", default_user, + "default-domain", default_domain, + "pids", pids, + "choices", choices, + NULL); +} + + +const char * +phosh_gtk_mount_prompt_get_password (PhoshGtkMountPrompt *self) +{ + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self), NULL); + + return gtk_entry_buffer_get_text (self->password_buffer); +} + + +GAskPasswordFlags +phosh_gtk_mount_prompt_get_ask_flags (PhoshGtkMountPrompt *self) +{ + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self), 0); + + return self->ask_flags; +} + + +/** + * phosh_gtk_mount_prompt_get_cancelled: + * @self: The #PhoshGtkMountPrompt + * + * Returns: %TRUE if the dialog was cancelled (e.g. via swipe + * away or pressing the Esc-key. + */ +gboolean +phosh_gtk_mount_prompt_get_cancelled (PhoshGtkMountPrompt *self) +{ + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self), FALSE); + + return self->cancelled; +} + + +/** + * phosh_gtk_mount_prompt_get_choices: + * @self: The #PhoshGtkMountPrompt + * + * Returns: (transfer none): The prompt's choices + */ +GStrv +phosh_gtk_mount_prompt_get_choices (PhoshGtkMountPrompt *self) +{ + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self), NULL); + + return self->choices; +} + + +int +phosh_gtk_mount_prompt_get_choice (PhoshGtkMountPrompt *self) +{ + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self), -1); + + return self->choice; +} + + +void +phosh_gtk_mount_prompt_set_pids (PhoshGtkMountPrompt *self, GVariant *pids) +{ + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (self)); + + if (self->pids == pids) + return; + + g_clear_pointer (&self->pids, g_variant_unref); + + if (pids) + self->pids = g_variant_ref (pids); + + /* TODO: update ui */ + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PIDS]); +} diff --git a/src/gtk-mount-prompt.h b/src/gtk-mount-prompt.h new file mode 100644 index 000000000..3ccdd2ede --- /dev/null +++ b/src/gtk-mount-prompt.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "system-modal-dialog.h" + +#define PHOSH_TYPE_GTK_MOUNT_PROMPT (phosh_gtk_mount_prompt_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshGtkMountPrompt, phosh_gtk_mount_prompt, PHOSH, GTK_MOUNT_PROMPT, PhoshSystemModalDialog) + +GtkWidget *phosh_gtk_mount_prompt_new (const char *message, + const char *icon_name, + const char *default_user, + const char *default_domain, + GVariant *pids, + const char *const *choices, + GAskPasswordFlags ask_flags); +const char *phosh_gtk_mount_prompt_get_password (PhoshGtkMountPrompt *self); +GAskPasswordFlags phosh_gtk_mount_prompt_get_ask_flags (PhoshGtkMountPrompt *self); +gboolean phosh_gtk_mount_prompt_get_cancelled (PhoshGtkMountPrompt *self); +int phosh_gtk_mount_prompt_get_choice (PhoshGtkMountPrompt *self); +GStrv phosh_gtk_mount_prompt_get_choices (PhoshGtkMountPrompt *self); +void phosh_gtk_mount_prompt_set_pids (PhoshGtkMountPrompt *self, GVariant *pids); diff --git a/src/hks-info.c b/src/hks-info.c new file mode 100644 index 000000000..c3e126fa6 --- /dev/null +++ b/src/hks-info.c @@ -0,0 +1,210 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-hks-info" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "hks-info.h" +#include "hks-manager.h" + +/** + * PhoshHksInfo: + * + * A widget to display the HKS status of a device + * + * #PhoshHksInfo displays whether a device is disabled via a hardware + * kill switch (HKS). + */ + +enum { + PROP_0, + PROP_DEV_TYPE, + PROP_BLOCKED, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshHksInfo { + PhoshStatusIcon parent; + + char *dev_type; + gboolean blocked; + gboolean present; + PhoshHksManager *manager; +}; +G_DEFINE_TYPE (PhoshHksInfo, phosh_hks_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_hks_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshHksInfo *self = PHOSH_HKS_INFO (object); + + switch (property_id) { + case PROP_BLOCKED: + g_value_set_boolean (value, self->blocked); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_hks_info_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshHksInfo *self = PHOSH_HKS_INFO (object); + + switch (prop_id) { + case PROP_DEV_TYPE: + /* construct only */ + self->dev_type = g_value_dup_string (value); + break; + case PROP_BLOCKED: + self->blocked = g_value_get_boolean (value); + break; + case PROP_PRESENT: + self->present = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +phosh_hks_info_constructed (GObject *object) +{ + PhoshHksInfo *self = PHOSH_HKS_INFO (object); + PhoshShell *shell; + g_autofree char *propname = NULL; + + G_OBJECT_CLASS (phosh_hks_info_parent_class)->constructed (object); + + shell = phosh_shell_get_default (); + self->manager = g_object_ref (phosh_shell_get_hks_manager (shell)); + + if (self->manager == NULL) { + g_warning ("Failed to get hks manager"); + return; + } + + propname = g_strdup_printf ("%s-present", self->dev_type); + g_object_bind_property (self->manager, + propname, + self, + "present", + G_BINDING_SYNC_CREATE); + + g_free (propname); + propname = g_strdup_printf ("%s-blocked", self->dev_type); + g_object_bind_property (self->manager, + propname, + self, + "blocked", + G_BINDING_SYNC_CREATE); + + g_free (propname); + propname = g_strdup_printf ("%s-icon-name", self->dev_type); + g_object_bind_property (self->manager, + propname, + self, + "icon-name", + G_BINDING_SYNC_CREATE); +} + + +static void +phosh_hks_info_dispose (GObject *object) +{ + PhoshHksInfo *self = PHOSH_HKS_INFO (object); + + g_clear_object (&self->manager); + + G_OBJECT_CLASS (phosh_hks_info_parent_class)->dispose (object); +} + + +static void +phosh_hks_info_finalize (GObject *object) +{ + PhoshHksInfo *self = PHOSH_HKS_INFO (object); + + g_clear_pointer (&self->dev_type, g_free); + + G_OBJECT_CLASS (phosh_hks_info_parent_class)->finalize (object); +} + + +static void +phosh_hks_info_class_init (PhoshHksInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_hks_info_constructed; + object_class->dispose = phosh_hks_info_dispose; + object_class->finalize = phosh_hks_info_finalize; + object_class->get_property = phosh_hks_info_get_property; + object_class->set_property = phosh_hks_info_set_property; + + gtk_widget_class_set_css_name (widget_class, "phosh-hks-info"); + + props[PROP_DEV_TYPE] = + g_param_spec_string ("device-type", + "Device type", + "The moniored device type", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_CONSTRUCT_ONLY); + props[PROP_BLOCKED] = + g_param_spec_boolean ("blocked", + "blocked", + "Whether the device is blocked", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + props[PROP_PRESENT] = + g_param_spec_boolean ("present", + "Present", + "Whether hks hardware is present", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_hks_info_init (PhoshHksInfo *self) +{ + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), "HKS"); +} + + +GtkWidget * +phosh_hks_info_new (void) +{ + return g_object_new (PHOSH_TYPE_HKS_INFO, NULL); +} diff --git a/src/hks-info.h b/src/hks-info.h new file mode 100644 index 000000000..1078fac59 --- /dev/null +++ b/src/hks-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_HKS_INFO (phosh_hks_info_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshHksInfo, phosh_hks_info, PHOSH, HKS_INFO, PhoshStatusIcon) + +GtkWidget * phosh_hks_info_new (void); + +G_END_DECLS diff --git a/src/hks-manager.c b/src/hks-manager.c new file mode 100644 index 000000000..f7b9a52f1 --- /dev/null +++ b/src/hks-manager.c @@ -0,0 +1,513 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-hks-manager" + +#include "phosh-config.h" + +#include "hks-manager.h" +#include "shell-priv.h" + +#include +#include +#include +#include +#include + + +#ifdef HAVE_RFKILL_EVENT_EXT +typedef struct rfkill_event_ext RfKillEvent; +#else +typedef struct rfkill_event RfKillEvent; +#endif +G_DEFINE_AUTOPTR_CLEANUP_FUNC (RfKillEvent, g_free); + +/* rfkill types not yet upstream */ +#define RFKILL_TYPE_CAMERA_ 9 +#define RFKILL_TYPE_MIC_ 10 + +/** + * PhoshHksManager: + * + * Tracks hardware kill switch state + * + * Monitor hardware kill switch state. This will be submitted to gnome-settings-daemon + * once we figured out the kernel interfaces. + */ + +typedef enum { + PROP_0, + /* Each hks must be in the order of present, blocked, icon_name */ + PROP_MIC_PRESENT, + PROP_MIC_BLOCKED, + PROP_MIC_ICON_NAME, + PROP_CAMERA_PRESENT, + PROP_CAMERA_BLOCKED, + PROP_CAMERA_ICON_NAME, + PROP_LAST_PROP +} PhoshHksManagerProps; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + HKS_STATE_BLOCKED = 0, + HKS_STATE_UNBLOCKED = 1, +}; + + +typedef struct { + gboolean present; + gboolean blocked; + char *blocked_icon_name; + char *unblocked_icon_name; + GHashTable *killswitches; +} Hks; + + +struct _PhoshHksManager { + GObject parent; + + GIOChannel *channel; + int watch_id; + + Hks mic; + Hks camera; +}; +G_DEFINE_TYPE (PhoshHksManager, phosh_hks_manager, G_TYPE_OBJECT); + + +static void +phosh_hks_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshHksManager *self = PHOSH_HKS_MANAGER (object); + const char *icon_name; + + switch (property_id) { + case PROP_MIC_PRESENT: + g_value_set_boolean (value, self->mic.present); + break; + case PROP_MIC_BLOCKED: + g_value_set_boolean (value, self->mic.blocked); + break; + case PROP_MIC_ICON_NAME: + icon_name = self->mic.blocked ? + self->mic.blocked_icon_name : self->mic.unblocked_icon_name; + g_value_set_string (value, icon_name); + break; + case PROP_CAMERA_PRESENT: + g_value_set_boolean (value, self->camera.present); + break; + case PROP_CAMERA_BLOCKED: + g_value_set_boolean (value, self->camera.blocked); + break; + case PROP_CAMERA_ICON_NAME: + icon_name = self->camera.blocked ? + self->camera.blocked_icon_name : self->camera.unblocked_icon_name; + g_value_set_string (value, icon_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static const char * +type_to_string (unsigned int type) +{ + switch (type) { + case RFKILL_TYPE_ALL: + return "ALL"; + case RFKILL_TYPE_WLAN: + return "WLAN"; + case RFKILL_TYPE_BLUETOOTH: + return "BLUETOOTH"; + case RFKILL_TYPE_UWB: + return "UWB"; + case RFKILL_TYPE_WIMAX: + return "WIMAX"; + case RFKILL_TYPE_WWAN: + return "WWAN"; + case RFKILL_TYPE_CAMERA_: + return "CAMERA"; + case RFKILL_TYPE_MIC_: + return "MIC"; + default: + return "UNKNOWN"; + } +} + + +static const char * +op_to_string (unsigned int op) +{ + switch (op) { + case RFKILL_OP_ADD: + return "ADD"; + case RFKILL_OP_DEL: + return "DEL"; + case RFKILL_OP_CHANGE: + return "CHANGE"; + case RFKILL_OP_CHANGE_ALL: + return "CHANGE_ALL"; + default: + return "Unknown"; + } +} + + +static void +print_event (RfKillEvent *event) +{ + g_debug ("RFKILL event: idx %u type %u (%s) op %u (%s) soft %u hard %u", + event->idx, + event->type, type_to_string (event->type), + event->op, op_to_string (event->op), + event->soft, event->hard); +} + + +static void +update_props (PhoshHksManager *self, Hks *hks, PhoshHksManagerProps prop) +{ + GHashTableIter iter; + gpointer key, value; + gboolean blocked = FALSE; + + if (g_hash_table_size (hks->killswitches) == 0) { + if (!hks->present) + return; + + if (hks->blocked) { + hks->blocked = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), props[prop+1]); + g_object_notify_by_pspec (G_OBJECT (self), props[prop+2]); + } + + hks->present = FALSE; + g_object_notify_by_pspec (G_OBJECT (self), props[prop]); + return; + } + + g_hash_table_iter_init (&iter, hks->killswitches); + while (g_hash_table_iter_next (&iter, &key, &value)) { + int state; + + state = GPOINTER_TO_INT (value); + /* A single blocked switch is enough */ + if (state == HKS_STATE_BLOCKED) { + blocked = TRUE; + break; + } + } + + if (!hks->present) { + hks->present = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[prop]); + } + + if (blocked == hks->blocked) + return; + hks->blocked = blocked; + + /* blocked property */ + g_object_notify_by_pspec (G_OBJECT (self), props[prop + 1]); + /* icon_name property */ + g_object_notify_by_pspec (G_OBJECT (self), props[prop + 2]); +} + + +static void +process_events (PhoshHksManager *self, GList *events) +{ + GList *l; + int value; + + for (l = events; l != NULL; l = l->next) { + RfKillEvent *event = l->data; + + switch (event->op) { + case RFKILL_OP_ADD: + case RFKILL_OP_CHANGE: + value = event->hard ? HKS_STATE_BLOCKED : HKS_STATE_UNBLOCKED; + + if (event->type == RFKILL_TYPE_MIC_) { + g_hash_table_insert (self->mic.killswitches, + GINT_TO_POINTER (event->idx), + GINT_TO_POINTER (value)); + } else if (event->type == RFKILL_TYPE_CAMERA_) { + g_hash_table_insert (self->camera.killswitches, + GINT_TO_POINTER (event->idx), + GINT_TO_POINTER (value)); + } + g_debug ("%s rfkill type %d, ID %d", + event->op == RFKILL_OP_ADD ? "Added" : "Changed", + event->type, event->idx); + break; + case RFKILL_OP_DEL: + if (event->type == RFKILL_TYPE_MIC_) { + g_hash_table_remove (self->mic.killswitches, + GINT_TO_POINTER (event->idx)); + } else if (event->type == RFKILL_TYPE_CAMERA_) { + g_hash_table_remove (self->camera.killswitches, + GINT_TO_POINTER (event->idx)); + } + g_debug ("Removed rfkill type %d, ID %d", + event->type, event->idx); + break; + default: + /* Nothing to do */ + break; + } + } + + g_object_freeze_notify (G_OBJECT (self)); + update_props (self, &self->mic, PROP_MIC_PRESENT); + update_props (self, &self->camera, PROP_CAMERA_PRESENT); + g_object_thaw_notify (G_OBJECT (self)); + + +} + + +static gboolean +rfkill_event_cb (GIOChannel *source, + GIOCondition condition, + PhoshHksManager *self) +{ + g_autolist (RfKillEvent) events = NULL; + + if (condition & G_IO_IN) { + GIOStatus status; + RfKillEvent event = { 0 }; + RfKillEvent *event_ptr; + gsize read; + + status = g_io_channel_read_chars (source, + (char *) &event, + sizeof(event), + &read, + NULL); + + while (status == G_IO_STATUS_NORMAL && read >= RFKILL_EVENT_SIZE_V1) { + print_event (&event); + event_ptr = g_memdup2 (&event, sizeof(event)); + events = g_list_prepend (events, event_ptr); + + status = g_io_channel_read_chars (source, + (char *) &event, + sizeof(event), + &read, + NULL); + } + events = g_list_reverse (events); + } else { + g_debug ("Something unexpected happened on rfkill fd"); + return FALSE; + } + + process_events (self, events); + return TRUE; +} + + +static gboolean +setup_rfkill (PhoshHksManager *self) +{ + int fd, ret; + g_autolist (RfKillEvent) events = NULL; + + fd = open ("/dev/rfkill", O_RDONLY); + if (fd < 0) + return FALSE; + + ret = fcntl (fd, F_SETFL, O_NONBLOCK); + if (ret < 0) { + g_warning ("Can't set RFKILL control device to non-blocking: %s", strerror (errno)); + close (fd); + return FALSE; + } + + while (1) { + RfKillEvent event; + RfKillEvent *event_ptr; + ssize_t len; + + len = read (fd, &event, sizeof(event)); + if (len < 0) { + if (errno == EAGAIN) + break; + g_debug ("Reading of RFKILL events failed"); + break; + } + + if (len < RFKILL_EVENT_SIZE_V1) { + g_warning ("Wrong size of RFKILL event\n"); + continue; + } + + if (event.op != RFKILL_OP_ADD) + continue; + + g_debug ("Read killswitch of type '%s' (idx=%d): soft %d hard %d", + type_to_string (event.type), + event.idx, event.soft, event.hard); + + event_ptr = g_memdup2 (&event, sizeof(event)); + events = g_list_prepend (events, event_ptr); + } + + /* Setup monitoring */ + self->channel = g_io_channel_unix_new (fd); + g_io_channel_set_encoding (self->channel, NULL, NULL); + g_io_channel_set_buffered (self->channel, FALSE); + self->watch_id = g_io_add_watch (self->channel, + G_IO_IN | G_IO_HUP | G_IO_ERR, + (GIOFunc) rfkill_event_cb, + self); + + if (events) { + events = g_list_reverse (events); + process_events (self, events); + } else { + g_debug ("No rfkill device available on startup"); + } + + return TRUE; +} + + +static void +phosh_hks_manager_constructed (GObject *object) +{ + PhoshHksManager *self = PHOSH_HKS_MANAGER (object); + + self->mic.killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + self->mic.blocked_icon_name = "microphone-hardware-disabled-symbolic"; + self->mic.unblocked_icon_name = "microphone-sensitivity-high-symbolic"; + + self->camera.killswitches = g_hash_table_new (g_direct_hash, g_direct_equal); + self->camera.blocked_icon_name = "camera-hardware-disabled-symbolic"; + self->camera.unblocked_icon_name = "camera-photo-symbolic"; + + setup_rfkill (self); + + G_OBJECT_CLASS (phosh_hks_manager_parent_class)->constructed (object); +} + + +static void +phosh_hks_manager_dispose (GObject *object) +{ + PhoshHksManager *self = PHOSH_HKS_MANAGER (object); + + if (self->watch_id > 0) { + g_source_remove (self->watch_id); + self->watch_id = 0; + g_io_channel_shutdown (self->channel, FALSE, NULL); + g_io_channel_unref (self->channel); + } + + G_OBJECT_CLASS (phosh_hks_manager_parent_class)->dispose (object); +} + + +static void +phosh_hks_manager_finalize (GObject *object) +{ + PhoshHksManager *self = PHOSH_HKS_MANAGER (object); + + g_clear_pointer (&self->mic.killswitches, g_hash_table_destroy); + g_clear_pointer (&self->camera.killswitches, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_hks_manager_parent_class)->finalize (object); +} + + +static void +phosh_hks_manager_class_init (PhoshHksManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_hks_manager_constructed; + object_class->dispose = phosh_hks_manager_dispose; + object_class->finalize = phosh_hks_manager_finalize; + object_class->get_property = phosh_hks_manager_get_property; + + props[PROP_MIC_PRESENT] = + g_param_spec_boolean ("mic-present", + "Mic present", + "HKS capable microphone present", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_MIC_BLOCKED] = + g_param_spec_boolean ("mic-blocked", + "Mic blocked", + "Microphone blocked via hks", + TRUE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_MIC_ICON_NAME] = + g_param_spec_string ("mic-icon-name", + "Mic Icon Name", + "Icon for microphone hks", + "", + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_CAMERA_PRESENT] = + g_param_spec_boolean ("camera-present", + "Camera present", + "HKS capable camera present", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_CAMERA_BLOCKED] = + g_param_spec_boolean ("camera-blocked", + "Camera blocked", + "Camera blocked via hks", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_CAMERA_ICON_NAME] = + g_param_spec_string ("camera-icon-name", + "Camera Icon Name", + "Icon for camera hks", + "", + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_hks_manager_init (PhoshHksManager *self) +{ +} + + +PhoshHksManager * +phosh_hks_manager_new (void) +{ + return PHOSH_HKS_MANAGER (g_object_new (PHOSH_TYPE_HKS_MANAGER, NULL)); +} diff --git a/src/hks-manager.h b/src/hks-manager.h new file mode 100644 index 000000000..7869b8c46 --- /dev/null +++ b/src/hks-manager.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + + +G_BEGIN_DECLS + +/** + * PhoshHksDeviceType: + * @PHOSH_HKS_TYPE_MIC: Microphone hardware kill switch + * + * Keep in sync with kernels rfkill types + */ +typedef enum { + PHOSH_HKS_TYPE_MIC = 10, +} PhoshHksDeviceType; + + +#define PHOSH_TYPE_HKS_MANAGER (phosh_hks_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshHksManager, phosh_hks_manager, PHOSH, HKS_MANAGER, GObject) + +PhoshHksManager *phosh_hks_manager_new (void); + +G_END_DECLS diff --git a/src/home.c b/src/home.c new file mode 100644 index 000000000..e08641008 --- /dev/null +++ b/src/home.c @@ -0,0 +1,789 @@ +/* + * Copyright (C) 2018-2022 Purism SPC + * 2023-2024 Guido Günther + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-home" + +#include "phosh-config.h" +#include "layersurface-priv.h" +#include "overview.h" +#include "home.h" +#include "shell-priv.h" +#include "phosh-enums.h" +#include "osk-manager.h" +#include "style-manager.h" +#include "feedback-manager.h" +#include "util.h" + +#include + +#define KEYBINDINGS_SCHEMA_ID "org.gnome.shell.keybindings" +#define KEYBINDING_KEY_TOGGLE_OVERVIEW "toggle-overview" +#define KEYBINDING_KEY_TOGGLE_APPLICATION_VIEW "toggle-application-view" + +#define PHOSH_SETTINGS "sm.puri.phosh" + +#define PHOSH_HOME_DRAG_THRESHOLD 0.3 + +#define POWERBAR_ACTIVE_CLASS "p-active" +#define POWERBAR_FAILED_CLASS "p-failed" + +/** + * PhoshHome: + * + * The home surface contains the overview and the home bar to fold and unfold the overview. + * + * #PhoshHome contains the #PhoshOverview that manages running + * applications and the app grid. It also manages a bar at the + * bottom of the screen to fold and unfold the #PhoshOverview and a + * pill in the center (powerbar) that toggles the OSK. + */ + +enum { + PROP_0, + PROP_HOME_STATE, + PROP_OSK_ENABLED, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshHome +{ + PhoshDragSurface parent; + + GtkWidget *overview; + GtkWidget *home_bar; + GtkWidget *rev_powerbar; + GtkWidget *powerbar; + GtkWidget *evbox_home_bar; + + guint debounce_handle; + gboolean focus_app_search; + + PhoshHomeState state; + + /* Keybinding */ + GStrv action_names; + GSettings *settings; + + /* osk button */ + gboolean osk_enabled; + + GtkGesture *click_gesture; /* needed so that the gesture isn't destroyed immediately */ + GtkGesture *osk_toggle_long_press; /* to toggle osk from the home bar itself */ + GSettings *phosh_settings; + + PhoshMonitor *monitor; + PhoshBackground *background; + gboolean use_background; +}; +G_DEFINE_TYPE(PhoshHome, phosh_home, PHOSH_TYPE_DRAG_SURFACE); + + +static void +phosh_home_update_home_bar (PhoshHome *self) +{ + gboolean reveal, solid = TRUE; + PhoshDragSurfaceState drag_state = phosh_drag_surface_get_drag_state (PHOSH_DRAG_SURFACE (self)); + + reveal = !(self->state == PHOSH_HOME_STATE_UNFOLDED); + gtk_revealer_set_reveal_child (GTK_REVEALER (self->rev_powerbar), reveal); + + if (self->use_background) + solid = !!(self->state == PHOSH_HOME_STATE_FOLDED && drag_state != PHOSH_DRAG_SURFACE_STATE_DRAGGED); + + phosh_util_toggle_style_class (self->evbox_home_bar, "p-solid", solid); +} + + +static void +phosh_home_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshHome *self = PHOSH_HOME (object); + + switch (property_id) { + case PROP_HOME_STATE: + phosh_home_set_state (self, g_value_get_enum (value)); + break; + case PROP_OSK_ENABLED: + self->osk_enabled = g_value_get_boolean (value); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_OSK_ENABLED]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_home_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshHome *self = PHOSH_HOME (object); + + switch (property_id) { + case PROP_HOME_STATE: + g_value_set_enum (value, self->state); + break; + case PROP_OSK_ENABLED: + g_value_set_boolean (value, self->osk_enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +update_drag_handle (PhoshHome *self, gboolean queue_draw) +{ + gboolean success; + gint handle = 0; + PhoshAppGrid *app_grid; + PhoshDragSurfaceDragMode drag_mode = PHOSH_DRAG_SURFACE_DRAG_MODE_HANDLE; + PhoshDragSurfaceState drag_state = phosh_drag_surface_get_drag_state (PHOSH_DRAG_SURFACE (self)); + + /* reset osk_toggle_long_press to prevent OSK from unfolding accidentally */ + gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->osk_toggle_long_press)); + + /* Update the handle's and dragability */ + if (phosh_overview_has_running_activities (PHOSH_OVERVIEW (self->overview)) == FALSE && + self->state == PHOSH_HOME_STATE_UNFOLDED && drag_state != PHOSH_DRAG_SURFACE_STATE_DRAGGED) { + drag_mode = PHOSH_DRAG_SURFACE_DRAG_MODE_NONE; + } + phosh_drag_surface_set_drag_mode (PHOSH_DRAG_SURFACE (self), drag_mode); + + /* Update handle size */ + app_grid = phosh_overview_get_app_grid (PHOSH_OVERVIEW (self->overview)); + success = gtk_widget_translate_coordinates (GTK_WIDGET (app_grid), + GTK_WIDGET (self), + 0, 0, NULL, &handle); + if (!success) { + g_warning ("Failed to get handle position"); + handle = PHOSH_HOME_BAR_HEIGHT; + } + + g_debug ("Drag Handle: %d", handle); + phosh_drag_surface_set_drag_handle (PHOSH_DRAG_SURFACE (self), handle); + /* Trigger redraw and surface commit */ + if (queue_draw) + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +static int +get_margin (gint height) +{ + return (-1 * height) + PHOSH_HOME_BAR_HEIGHT; +} + + +static gboolean +on_configure_event (PhoshHome *self, GdkEventConfigure *event) +{ + guint margin; + + margin = get_margin (event->height); + + /* ignore popovers like the power menu */ + if (gtk_widget_get_window (GTK_WIDGET (self)) != event->window) + return FALSE; + + g_debug ("%s: %dx%d, margin: %d", __func__, event->height, event->width, margin); + + /* If the size changes we need to update the folded margin */ + phosh_drag_surface_set_margin (PHOSH_DRAG_SURFACE (self), margin, 0); + /* Update drag handle since overview size might have changed */ + update_drag_handle (self, TRUE); + + return FALSE; +} + + +static void +phosh_home_map (GtkWidget *widget) +{ + PhoshHome *self = PHOSH_HOME (widget); + + GTK_WIDGET_CLASS (phosh_home_parent_class)->map (widget); + + phosh_layer_surface_set_stacked_below (PHOSH_LAYER_SURFACE (self->background), + PHOSH_LAYER_SURFACE (self)); +} + + +static void +on_home_released (GtkButton *button, int n_press, double x, double y, GtkGestureMultiPress *gesture) +{ + PhoshHome *self = g_object_get_data (G_OBJECT (gesture), "phosh-home"); + + g_return_if_fail (PHOSH_IS_HOME (self)); + + if (phosh_util_gesture_is_touch (GTK_GESTURE_SINGLE (gesture)) == FALSE) + phosh_home_set_state (self, !self->state); +} + + +static void +on_powerbar_action_started (PhoshHome *self) +{ + g_debug ("powerbar action started"); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_FAILED_CLASS, FALSE); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_ACTIVE_CLASS, TRUE); +} + + +static void +on_powerbar_action_ended (PhoshHome *self) +{ + g_debug ("powerbar action ended"); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_ACTIVE_CLASS, FALSE); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_FAILED_CLASS, FALSE); +} + + +static void +on_powerbar_action_failed (PhoshHome *self) +{ + g_debug ("powerbar action failed"); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_ACTIVE_CLASS, FALSE); + phosh_util_toggle_style_class (self->home_bar, POWERBAR_FAILED_CLASS, TRUE); +} + + +static void +on_powerbar_pressed (PhoshHome *self) +{ + PhoshOskManager *osk; + gboolean osk_is_available, osk_current_state, osk_new_state; + + g_return_if_fail (PHOSH_IS_HOME (self)); + + osk = phosh_shell_get_osk_manager (phosh_shell_get_default ()); + + osk_is_available = phosh_osk_manager_get_available (osk); + osk_current_state = phosh_osk_manager_get_visible (osk); + osk_new_state = osk_current_state; + + gtk_gesture_set_state ((self->click_gesture), GTK_EVENT_SEQUENCE_DENIED); + + if (osk_is_available) { + osk_new_state = !osk_current_state; + on_powerbar_action_ended (self); + } else { + on_powerbar_action_failed (self); + return; + } + + g_debug ("OSK toggled with pressed signal"); + phosh_osk_manager_set_visible (osk, osk_new_state); + + phosh_trigger_feedback ("button-pressed"); +} + + +static void +fold_cb (PhoshHome *self, PhoshOverview *overview) +{ + g_return_if_fail (PHOSH_IS_HOME (self)); + g_return_if_fail (PHOSH_IS_OVERVIEW (overview)); + + phosh_home_set_state (self, PHOSH_HOME_STATE_FOLDED); +} + + +static void +delayed_handle_resize (gpointer data) +{ + PhoshHome *self = PHOSH_HOME (data); + + self->debounce_handle = 0; + update_drag_handle (self, TRUE); +} + + +static void +on_has_activities_changed (PhoshHome *self) +{ + g_return_if_fail (PHOSH_IS_HOME (self)); + + /* TODO: we need to debounce the handle resize a little until all + the queued resizing is done, would be nicer to have that tied to + a signal */ + self->debounce_handle = g_timeout_add_once (200, delayed_handle_resize, self); + g_source_set_name_by_id (self->debounce_handle, "[phosh] delayed_handle_resize"); +} + + +static gboolean +window_key_press_event_cb (PhoshHome *self, GdkEvent *event, gpointer data) +{ + gboolean ret = GDK_EVENT_PROPAGATE; + guint keyval; + g_return_val_if_fail (PHOSH_IS_HOME (self), GDK_EVENT_PROPAGATE); + + if (self->state != PHOSH_HOME_STATE_UNFOLDED) + return GDK_EVENT_PROPAGATE; + + if (!gdk_event_get_keyval (event, &keyval)) + return GDK_EVENT_PROPAGATE; + + switch (keyval) { + case GDK_KEY_Escape: + phosh_home_set_state (self, PHOSH_HOME_STATE_FOLDED); + ret = GDK_EVENT_STOP; + break; + case GDK_KEY_Return: + ret = GDK_EVENT_PROPAGATE; + break; + default: + /* Focus search when typing */ + ret = phosh_overview_handle_search (PHOSH_OVERVIEW (self->overview), event); + } + + return ret; +} + + +static void +toggle_overview_action (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshHome *self = PHOSH_HOME (data); + PhoshHomeState state; + + g_return_if_fail (PHOSH_IS_HOME (self)); + + state = self->state == PHOSH_HOME_STATE_UNFOLDED ? + PHOSH_HOME_STATE_FOLDED : PHOSH_HOME_STATE_UNFOLDED; + phosh_home_set_state (self, state); +} + + +static void +toggle_application_view_action (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshHome *self = PHOSH_HOME (data); + PhoshHomeState state; + + g_return_if_fail (PHOSH_IS_HOME (self)); + + state = self->state == PHOSH_HOME_STATE_UNFOLDED ? + PHOSH_HOME_STATE_FOLDED : PHOSH_HOME_STATE_UNFOLDED; + phosh_home_set_state (self, state); + + /* Focus app search once unfolded */ + if (state == PHOSH_HOME_STATE_UNFOLDED) + self->focus_app_search = TRUE; +} + + +static void +add_keybindings (PhoshHome *self) +{ + const GActionEntry super_entries[] = { + { "Super_R", .activate = toggle_overview_action }, + { "Super_L", .activate = toggle_overview_action }, + }; + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + g_autoptr (GSettings) settings = g_settings_new (KEYBINDINGS_SCHEMA_ID); + g_autoptr (GArray) actions = g_array_new (FALSE, TRUE, sizeof (GActionEntry)); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + settings, + KEYBINDING_KEY_TOGGLE_OVERVIEW, + toggle_overview_action); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + settings, + KEYBINDING_KEY_TOGGLE_APPLICATION_VIEW, + toggle_application_view_action); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry*) actions->data, + actions->len, + self); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry*)super_entries, + G_N_ELEMENTS (super_entries), + self); + + for (int i = 0; i < G_N_ELEMENTS (super_entries); i++) + g_strv_builder_add (builder, super_entries[i].name); + + self->action_names = g_strv_builder_end (builder); +} + + +static void +on_keybindings_changed (PhoshHome *self, + char *key, + GSettings *settings) +{ + /* For now just redo all keybindings */ + g_debug ("Updating keybindings"); + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + add_keybindings (self); +} + + +static void +phosh_home_set_background_alpha (PhoshHome *self, double alpha) +{ + if (self->background) + phosh_layer_surface_set_alpha (PHOSH_LAYER_SURFACE (self->background), alpha); +} + + +static void +phosh_home_dragged (PhoshDragSurface *drag_surface, int margin) +{ + PhoshHome *self = PHOSH_HOME (drag_surface); + int width, height; + double progress, alpha; + + gtk_window_get_size (GTK_WINDOW (self), &width, &height); + progress = 1.0 - (-margin / (double)(height - PHOSH_HOME_BAR_HEIGHT)); + /* Avoid negative values when resizing the surface */ + progress = MAX (0, progress); + + alpha = hdy_ease_out_cubic (progress); + phosh_home_set_background_alpha (self, alpha); +} + + +static void +on_drag_state_changed (PhoshHome *self) +{ + PhoshHomeState state = self->state; + PhoshDragSurfaceState drag_state; + gboolean kbd_interactivity = FALSE; + + drag_state = phosh_drag_surface_get_drag_state (PHOSH_DRAG_SURFACE (self)); + + switch (drag_state) { + case PHOSH_DRAG_SURFACE_STATE_UNFOLDED: + state = PHOSH_HOME_STATE_UNFOLDED; + kbd_interactivity = TRUE; + if (self->focus_app_search) { + phosh_overview_focus_app_search (PHOSH_OVERVIEW (self->overview)); + self->focus_app_search = FALSE; + } + phosh_home_set_background_alpha (self, 1.0); + break; + case PHOSH_DRAG_SURFACE_STATE_FOLDED: + state = PHOSH_HOME_STATE_FOLDED; + phosh_home_set_background_alpha (self, 0.0); + phosh_overview_reset (PHOSH_OVERVIEW (self->overview)); + break; + case PHOSH_DRAG_SURFACE_STATE_DRAGGED: + state = PHOSH_HOME_STATE_TRANSITION; + if (self->state == PHOSH_HOME_STATE_FOLDED) + phosh_overview_refresh (PHOSH_OVERVIEW (self->overview)); + break; + default: + g_return_if_reached (); + return; + } + + if (self->state != state) { + self->state = state; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOME_STATE]); + } + + phosh_home_update_home_bar (self); + + phosh_layer_surface_set_kbd_interactivity (PHOSH_LAYER_SURFACE (self), kbd_interactivity); + update_drag_handle (self, TRUE); +} + + +static void +phosh_home_add_background (PhoshHome *self) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitor *monitor; + cairo_rectangle_int_t rect = { 0, 0, 0, 0 }; + cairo_region_t *region; + + monitor = phosh_shell_get_primary_monitor (shell); + self->background = PHOSH_BACKGROUND (phosh_background_new ( + phosh_wayland_get_zwlr_layer_shell_v1 (wl), + monitor, + /* Span over whole display */ + FALSE, + ZWLR_LAYER_SHELL_V1_LAYER_TOP)); + g_object_bind_property (self, "visible", self->background, "visible", G_BINDING_SYNC_CREATE); + + g_signal_connect_object (phosh_shell_get_background_manager (shell), + "config-changed", + G_CALLBACK (phosh_background_needs_update), + self->background, + G_CONNECT_SWAPPED); + + region = cairo_region_create_rectangle (&rect); + gtk_widget_input_shape_combine_region (GTK_WIDGET (self->background), region); + cairo_region_destroy (region); +} + + +static void +on_theme_name_changed (PhoshHome *self, GParamSpec *pspec, PhoshStyleManager *style_manager) +{ + g_assert (PHOSH_IS_HOME (self)); + g_assert (PHOSH_IS_STYLE_MANAGER (style_manager)); + + self->use_background = !phosh_style_manager_is_high_contrast (style_manager); + phosh_util_toggle_style_class (GTK_WIDGET (self), "p-solid", !self->use_background); + if (gtk_widget_get_visible (GTK_WIDGET (self))) + gtk_widget_set_visible (GTK_WIDGET (self->background), self->use_background); + + phosh_home_update_home_bar (self); +} + + +static void +phosh_home_constructed (GObject *object) +{ + PhoshHome *self = PHOSH_HOME (object); + PhoshShell *shell = phosh_shell_get_default (); + PhoshOskManager *osk_manager; + + G_OBJECT_CLASS (phosh_home_parent_class)->constructed (object); + + g_object_connect (self->settings, + "swapped-signal::changed::" KEYBINDING_KEY_TOGGLE_OVERVIEW, + on_keybindings_changed, self, + "swapped-signal::changed::" KEYBINDING_KEY_TOGGLE_APPLICATION_VIEW, + on_keybindings_changed, self, + NULL); + add_keybindings (self); + + osk_manager = phosh_shell_get_osk_manager (phosh_shell_get_default ()); + g_object_bind_property (osk_manager, "available", + self, "osk-enabled", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_signal_connect (self, "notify::drag-state", G_CALLBACK (on_drag_state_changed), NULL); + + g_object_set_data (G_OBJECT (self->click_gesture), "phosh-home", self); + g_object_set_data (G_OBJECT (self->osk_toggle_long_press), "phosh-home", self); + + phosh_home_add_background (self); + g_signal_connect_object (phosh_shell_get_style_manager (shell), + "notify::theme-name", + G_CALLBACK (on_theme_name_changed), + self, + G_CONNECT_SWAPPED); + on_theme_name_changed (self, NULL, phosh_shell_get_style_manager (shell)); +} + + +static void +phosh_home_dispose (GObject *object) +{ + PhoshHome *self = PHOSH_HOME (object); + + g_clear_object (&self->settings); + + if (self->action_names) { + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + } + g_clear_handle_id (&self->debounce_handle, g_source_remove); + + g_clear_pointer (&self->background, phosh_cp_widget_destroy); + + G_OBJECT_CLASS (phosh_home_parent_class)->dispose (object); +} + + +static void +phosh_home_class_init (PhoshHomeClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + PhoshDragSurfaceClass *drag_surface_class = PHOSH_DRAG_SURFACE_CLASS (klass); + + object_class->constructed = phosh_home_constructed; + object_class->dispose = phosh_home_dispose; + + object_class->set_property = phosh_home_set_property; + object_class->get_property = phosh_home_get_property; + + widget_class->map = phosh_home_map; + + drag_surface_class->dragged = phosh_home_dragged; + + /** + * PhoshHome:state: + * + * Whether the home widget is currently folded (only home-bar is + * visible) or unfolded (overview is visible). The property is + * changed when the widget reaches it's target state. + */ + props[PROP_HOME_STATE] = + g_param_spec_enum ("state", "", "", + PHOSH_TYPE_HOME_STATE, + PHOSH_HOME_STATE_FOLDED, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshHome:osk-enabled: + * + * Whether the osk is currently enabled in the system configuration. + */ + props[PROP_OSK_ENABLED] = + g_param_spec_boolean ("osk-enabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + g_type_ensure (PHOSH_TYPE_OVERVIEW); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/home.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, click_gesture); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, evbox_home_bar); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, home_bar); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, osk_toggle_long_press); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, overview); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, rev_powerbar); + gtk_widget_class_bind_template_child (widget_class, PhoshHome, powerbar); + gtk_widget_class_bind_template_callback (widget_class, fold_cb); + gtk_widget_class_bind_template_callback (widget_class, on_home_released); + gtk_widget_class_bind_template_callback (widget_class, on_has_activities_changed); + gtk_widget_class_bind_template_callback (widget_class, on_powerbar_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_powerbar_action_started); + gtk_widget_class_bind_template_callback (widget_class, on_powerbar_action_ended); + gtk_widget_class_bind_template_callback (widget_class, window_key_press_event_cb); + + gtk_widget_class_set_css_name (widget_class, "phosh-home"); +} + + +static void +phosh_home_init (PhoshHome *self) +{ + g_autoptr (GSettings) settings = NULL; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->use_background = TRUE; + self->state = PHOSH_HOME_STATE_FOLDED; + self->settings = g_settings_new (KEYBINDINGS_SCHEMA_ID); + + /* Adjust margins and folded state on size changes */ + g_signal_connect (self, "configure-event", G_CALLBACK (on_configure_event), NULL); + + settings = g_settings_new (PHOSH_SETTINGS); + g_settings_bind (settings, "osk-unfold-delay", + self->osk_toggle_long_press, "delay-factor", + G_SETTINGS_BIND_GET); +} + + +GtkWidget * +phosh_home_new (struct zwlr_layer_shell_v1 *layer_shell, + struct zphoc_layer_shell_effects_v1 *layer_shell_effects, + PhoshMonitor *monitor) +{ + return g_object_new (PHOSH_TYPE_HOME, + /* layer-surface */ + "layer-shell", layer_shell, + "wl-output", monitor->wl_output, + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, + "layer", ZWLR_LAYER_SHELL_V1_LAYER_TOP, + "kbd-interactivity", FALSE, + "exclusive-zone", PHOSH_HOME_BAR_HEIGHT, + "namespace", "phosh home", + /* drag-surface */ + "layer-shell-effects", layer_shell_effects, + "exclusive", PHOSH_HOME_BAR_HEIGHT, + "threshold", PHOSH_HOME_DRAG_THRESHOLD, + NULL); +} + +/** + * phosh_home_get_state: + * @self: The home surface + * + * Get the current state of the home widget. See [property@Home:state] for details. + * + * Returns: The home widget's state + */ +PhoshHomeState +phosh_home_get_state (PhoshHome *self) +{ + g_return_val_if_fail (PHOSH_IS_HOME (self), PHOSH_HOME_STATE_FOLDED); + + return self->state; +} + +/** + * phosh_home_set_state: + * @self: The home surface + * @state: The state to set + * + * Set the state of the home screen. See #PhoshHomeState. + */ +void +phosh_home_set_state (PhoshHome *self, PhoshHomeState state) +{ + g_autofree char *state_name = NULL; + PhoshDragSurfaceState drag_state = phosh_drag_surface_get_drag_state (PHOSH_DRAG_SURFACE (self)); + PhoshDragSurfaceState target_state = PHOSH_DRAG_SURFACE_STATE_FOLDED; + + g_return_if_fail (PHOSH_IS_HOME (self)); + + if (self->state == state) + return; + + if (drag_state == PHOSH_DRAG_SURFACE_STATE_DRAGGED) + return; + + state_name = g_enum_to_string (PHOSH_TYPE_HOME_STATE, state); + g_debug ("Setting state to %s", state_name); + + if (state == PHOSH_HOME_STATE_UNFOLDED) + target_state = PHOSH_DRAG_SURFACE_STATE_UNFOLDED; + + phosh_drag_surface_set_drag_state (PHOSH_DRAG_SURFACE (self), target_state); +} + +/** + * phosh_home_get_overview: + * @self: The home surface + * + * Get the overview widget + * + * Returns:(transfer none): The overview + */ +PhoshOverview* +phosh_home_get_overview (PhoshHome *self) +{ + g_return_val_if_fail (PHOSH_IS_HOME (self), NULL); + + return PHOSH_OVERVIEW (self->overview); +} diff --git a/src/home.h b/src/home.h new file mode 100644 index 000000000..f049a7f48 --- /dev/null +++ b/src/home.h @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "drag-surface.h" +#include "monitor/monitor.h" +#include "overview.h" + +#include + +#define PHOSH_TYPE_HOME (phosh_home_get_type()) + +#define PHOSH_HOME_BAR_HEIGHT 15 + +/** + * PhoshHomeState: + * @PHOSH_HOME_STATE_FOLDED: Only home button is visible + * @PHOSH_HOME_STATE_UNFOLDED: Home unfolded, overview visible + * @PHOSH_HOME_STATE_TRANSITION: Home screen is transitioning between folded and unfolded + * + * The state of #PhoshHome. + */ +typedef enum { + PHOSH_HOME_STATE_FOLDED, + PHOSH_HOME_STATE_UNFOLDED, + PHOSH_HOME_STATE_TRANSITION, +} PhoshHomeState; + +G_DECLARE_FINAL_TYPE (PhoshHome, phosh_home, PHOSH, HOME, PhoshDragSurface) + +GtkWidget *phosh_home_new (struct zwlr_layer_shell_v1 *layer_shell, + struct zphoc_layer_shell_effects_v1 *layer_shell_effects, + PhoshMonitor *monitor); +PhoshHomeState phosh_home_get_state (PhoshHome *self); +void phosh_home_set_state (PhoshHome *self, PhoshHomeState state); +PhoshOverview *phosh_home_get_overview (PhoshHome *self); diff --git a/src/idle-manager.c b/src/idle-manager.c new file mode 100644 index 000000000..13868d68c --- /dev/null +++ b/src/idle-manager.c @@ -0,0 +1,455 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Somewhat based on mutter's src/backends/meta-idle-monitor-dbus.c + */ + +#define G_LOG_DOMAIN "phosh-idle-manager" + +#include "idle-manager.h" +#include "shell-priv.h" + +#include + +/** + * PhoshIdleManager: + * + * The idle manager singleton + * + * This uses ext_idle_notification_v1 Wayland protocol to + * implement mutter's org.gnome.Mutter.IdleMonitor DBus + * interface. Since we don't have per monitor information we only care + * about core. + * + * Each DBus watch either notifies on idle *or* on activity. + */ + +/* A DBus watch corresponding to either an idle or active timer */ +typedef struct { + /* DBus */ + PhoshIdleDBusIdleMonitor *dbus_monitor; + char *dbus_name; + guint watch_id; + guint name_watcher_id; + /* Whether this watch reports on active or on idle */ + gboolean active; + + /* Wayland */ + struct ext_idle_notification_v1 *idle_noti; + guint32 interval; +} DBusWatch; + + +/* The IdleManager maintains all watches */ +typedef struct _PhoshIdleManager +{ + GObject parent; + + GHashTable *watches; + GDBusObjectManagerServer *manager; + int dbus_name_id; +} PhoshIdleManager; + + +G_DEFINE_TYPE (PhoshIdleManager, phosh_idle_manager, G_TYPE_OBJECT); + + +static guint32 +get_next_dbus_watch_serial (void) +{ + static guint32 serial = 1; + g_atomic_int_inc (&serial); + return serial; +} + + +/* cleanup a single watch */ +static void +watch_dispose (DBusWatch *watch) +{ + ext_idle_notification_v1_destroy (watch->idle_noti); + g_bus_unwatch_name (watch->name_watcher_id); + g_object_unref (watch->dbus_monitor); + g_free (watch->dbus_name); + g_free (watch); +} + + +/* remove a watch from the list of known watches */ +static void +watch_remove (DBusWatch *watch) +{ + PhoshIdleManager *self = phosh_idle_manager_get_default (); + + g_debug ("Removing watch %d", watch->watch_id); + g_hash_table_remove (self->watches, &watch->watch_id); +} + + +static void +idle_notification_idled_cb (void *data, struct ext_idle_notification_v1 *timer) +{ + DBusWatch *watch = data; + GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON (watch->dbus_monitor); + + if (watch->active) + return; + + g_debug ("Idle Timer %d fired on %s", watch->watch_id, watch->dbus_name); + g_dbus_connection_emit_signal (g_dbus_interface_skeleton_get_connection (skeleton), + watch->dbus_name, + g_dbus_interface_skeleton_get_object_path (skeleton), + "org.gnome.Mutter.IdleMonitor", + "WatchFired", + g_variant_new ("(u)", watch->watch_id), + NULL); +} + + +static void +idle_notification_resumed_cb (void* data, struct ext_idle_notification_v1 *timer) +{ + DBusWatch *watch = data; + GDBusInterfaceSkeleton *skeleton = G_DBUS_INTERFACE_SKELETON (watch->dbus_monitor); + + if (!watch->active) + return; + + g_debug ("Active Timer %d fired", watch->watch_id); + g_dbus_connection_emit_signal (g_dbus_interface_skeleton_get_connection (skeleton), + watch->dbus_name, + g_dbus_interface_skeleton_get_object_path (skeleton), + "org.gnome.Mutter.IdleMonitor", + "WatchFired", + g_variant_new ("(u)", watch->watch_id), + NULL); + watch_remove (watch); +} + + +static const struct ext_idle_notification_v1_listener idle_notification_listener = { + .idled = idle_notification_idled_cb, + .resumed = idle_notification_resumed_cb, +}; + + +static void +name_vanished_callback (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + watch_remove ((DBusWatch*)user_data); +} + + +static DBusWatch * +watch_new (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation, + guint32 interval, + gboolean active) +{ + DBusWatch *watch; + guint32 watch_id; + PhoshWayland *wl = phosh_wayland_get_default (); + struct ext_idle_notification_v1 *idle_noti; + struct ext_idle_notifier_v1 *idle_manager = phosh_wayland_get_ext_idle_notifier_v1 (wl); + + watch_id = get_next_dbus_watch_serial (); + g_return_val_if_fail (watch_id != 0, NULL); /* protect against wrap around */ + idle_noti = ext_idle_notifier_v1_get_idle_notification (idle_manager, + interval, + phosh_wayland_get_wl_seat (wl)); + g_assert (idle_noti); + + watch = g_new0 (DBusWatch, 1); + watch->interval = interval; + watch->active = active; + watch->watch_id = watch_id; + watch->idle_noti = idle_noti; + watch->dbus_monitor = g_object_ref (skeleton); + watch->dbus_name = g_strdup (g_dbus_method_invocation_get_sender (invocation)); + watch->name_watcher_id = g_bus_watch_name_on_connection ( + g_dbus_method_invocation_get_connection (invocation), + watch->dbus_name, + G_BUS_NAME_WATCHER_FLAGS_NONE, + NULL, /* appeared */ + name_vanished_callback, + watch, NULL); + + ext_idle_notification_v1_add_listener (watch->idle_noti, &idle_notification_listener, watch); + + return watch; +} + + +static DBusWatch * +idle_watch_new (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation, + guint32 interval) +{ + DBusWatch *watch; + + watch = watch_new (skeleton, invocation, interval, FALSE); + g_return_val_if_fail (watch, NULL); + + return watch; +} + + + +static DBusWatch * +active_watch_new (PhoshIdleDBusIdleMonitor *skeleton, GDBusMethodInvocation *invocation) +{ + DBusWatch *watch; + + /* Use a idle timer of 0 since we're only interested in the active timer */ + watch = watch_new (skeleton, invocation, 0, TRUE); + g_return_val_if_fail (watch, NULL); + + return watch; +} + + +static gboolean +handle_add_idle_watch (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation, + guint64 arg_interval) +{ + DBusWatch *watch; + PhoshIdleManager *self = phosh_idle_manager_get_default (); + + /* The wayland protocol uses an unsigned int */ + if (arg_interval > G_MAXUINT32) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "interval %" G_GUINT64_FORMAT " > %" G_GUINT32_FORMAT, + arg_interval, G_MAXUINT32); + return TRUE; + } + watch = idle_watch_new (skeleton, invocation, arg_interval); + if (!watch) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_LIMITS_EXCEEDED, + "Failed to create watch"); + return TRUE; + } + + g_debug ("Created idle-timer %d for %" G_GUINT64_FORMAT " msec", watch->watch_id, arg_interval); + g_hash_table_insert (self->watches, &watch->watch_id, watch); + phosh_idle_dbus_idle_monitor_complete_add_idle_watch (skeleton, invocation, watch->watch_id); + return TRUE; +} + + +static gboolean +handle_add_user_active_watch (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation) +{ + DBusWatch *watch; + PhoshIdleManager *self = phosh_idle_manager_get_default (); + + watch = active_watch_new (skeleton, invocation); + if (!watch) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_LIMITS_EXCEEDED, + "Failed to create watch"); + return TRUE; + } + + g_debug ("Creating active timer %d", watch->watch_id); + g_hash_table_insert (self->watches, &watch->watch_id, watch); + phosh_idle_dbus_idle_monitor_complete_add_user_active_watch ( + skeleton, invocation, watch->watch_id); + return TRUE; +} + + +static gboolean +handle_get_idle_time (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation) +{ + g_debug ("Unimplemented DBus call %s", __func__); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_NOT_SUPPORTED, + "Not supported"); + return TRUE; +} + + +static gboolean +handle_remove_watch (PhoshIdleDBusIdleMonitor *skeleton, + GDBusMethodInvocation *invocation, + guint arg_id) +{ + PhoshIdleManager *self = phosh_idle_manager_get_default (); + + g_debug ("Removing watch %d", arg_id); + g_hash_table_remove (self->watches, &arg_id); + + phosh_idle_dbus_idle_monitor_complete_remove_watch (skeleton, invocation); + return TRUE; +} + + +static void +create_monitor_skeleton (GDBusObjectManagerServer *manager, + const char *path) +{ + g_autoptr(GDBusObjectSkeleton) object = NULL; + g_autoptr(GDBusInterfaceSkeleton) interface = NULL; + + object = G_DBUS_OBJECT_SKELETON (phosh_idle_dbus_object_skeleton_new (path)); + interface = G_DBUS_INTERFACE_SKELETON ( + phosh_idle_dbus_idle_monitor_skeleton_new ()); + + g_signal_connect (interface, "handle-add-idle-watch", + G_CALLBACK (handle_add_idle_watch), NULL); + g_signal_connect (interface, "handle-add-user-active-watch", + G_CALLBACK (handle_add_user_active_watch), NULL); + g_signal_connect (interface, "handle-remove-watch", + G_CALLBACK (handle_remove_watch), NULL); + g_signal_connect (interface, "handle-get-idletime", + G_CALLBACK (handle_get_idle_time), NULL); + + g_dbus_object_skeleton_add_interface (object, interface); + g_dbus_object_manager_server_export (manager, object); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Acquired name %s", name); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshIdleManager *self = PHOSH_IDLE_MANAGER (user_data); + g_autofree char *path = NULL; + + /* We need to use Mutter's object path here to make gnome-session happy */ + self->manager = g_dbus_object_manager_server_new ("/org/gnome/Mutter/IdleMonitor"); + + /* We never clear the core monitor, as that's supposed to cumulate + idle times from all devices */ + path = g_strdup ("/org/gnome/Mutter/IdleMonitor/Core"); + create_monitor_skeleton (self->manager, path); + + g_dbus_object_manager_server_set_connection (self->manager, connection); +} + + +static void +phosh_idle_manager_dispose (GObject *object) +{ + PhoshIdleManager *self = PHOSH_IDLE_MANAGER (object); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + g_clear_pointer (&self->watches, g_hash_table_destroy); + g_clear_object (&self->manager); + G_OBJECT_CLASS (phosh_idle_manager_parent_class)->dispose (object); +} + +void +phosh_idle_manager_reset_timers (PhoshIdleManager *self) +{ + GHashTableIter iter; + DBusWatch *watch; + PhoshWayland *wl = phosh_wayland_get_default (); + struct ext_idle_notifier_v1 *idle_manager = phosh_wayland_get_ext_idle_notifier_v1 (wl); + + g_return_if_fail (PHOSH_IS_IDLE_MANAGER (self)); + + g_debug ("Resetting idle timers"); + + g_hash_table_iter_init (&iter, self->watches); + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &watch)) { + if (watch->active) + continue; + + /* Recreate the idle timers to reset their interval */ + ext_idle_notification_v1_destroy (watch->idle_noti); + watch->idle_noti = ext_idle_notifier_v1_get_idle_notification (idle_manager, + watch->interval, + phosh_wayland_get_wl_seat (wl)); + ext_idle_notification_v1_add_listener (watch->idle_noti, &idle_notification_listener, watch); + } +} + +static void +phosh_idle_manager_constructed (GObject *object) +{ + PhoshIdleManager *self = PHOSH_IDLE_MANAGER (object); + + G_OBJECT_CLASS (phosh_idle_manager_parent_class)->constructed (object); + + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.gnome.Mutter.IdleMonitor", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT + | G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, NULL); + + self->watches = g_hash_table_new_full (g_int_hash, + g_int_equal, + NULL, + (GDestroyNotify) watch_dispose); +} + + +static void +phosh_idle_manager_class_init (PhoshIdleManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_idle_manager_constructed; + object_class->dispose = phosh_idle_manager_dispose; +} + + +static void +phosh_idle_manager_init (PhoshIdleManager *self) +{ + self->dbus_name_id = 0; +} + +/** + * phosh_idle_manager_get_default: + * + * Get the idle manager singleton + * + * Returns:(transfer none): The idle manager + */ +PhoshIdleManager * +phosh_idle_manager_get_default (void) +{ + static PhoshIdleManager *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_IDLE_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} diff --git a/src/idle-manager.h b/src/idle-manager.h new file mode 100644 index 000000000..c9b5e945d --- /dev/null +++ b/src/idle-manager.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include +#include "dbus/phosh-idle-dbus.h" + +#define PHOSH_TYPE_IDLE_MANAGER (phosh_idle_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshIdleManager, phosh_idle_manager, PHOSH, IDLE_MANAGER, GObject) + +PhoshIdleManager * phosh_idle_manager_get_default (void); +void phosh_idle_manager_reset_timers (PhoshIdleManager *self); diff --git a/src/keyboard-events.c b/src/keyboard-events.c new file mode 100644 index 000000000..a99e94eea --- /dev/null +++ b/src/keyboard-events.c @@ -0,0 +1,314 @@ +/* + * Copyright (C) 2020 Evangelos Ribeiro Tzaras + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Evangelos Ribeiro Tzaras + */ + +#define G_LOG_DOMAIN "phosh-keyboard-events" + +#include "keyboard-events.h" +#include "wlr-screencopy-unstable-v1-client-protocol.h" +#include "phosh-private-client-protocol.h" +#include "phosh-wayland.h" + +/** + * PhoshKeyboardEvents: + * + * Grabs and manages special keyboard events + */ + +enum { + PRESSED, + RELEASED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +struct _PhoshKeyboardEvents { + GSimpleActionGroup parent; + + struct phosh_private_keyboard_event *kbevent; + GHashTable *accelerators; +}; + +static void initable_iface_init (GInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshKeyboardEvents, phosh_keyboard_events, G_TYPE_SIMPLE_ACTION_GROUP, + G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, initable_iface_init)); + +static void +handle_accelerator_activated_event (void *data, + struct phosh_private_keyboard_event *kbevent, + uint32_t action_id, + uint32_t timestamp) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (data); + const char *action; + GVariant *pressed = NULL; + + action = g_hash_table_lookup (self->accelerators, GUINT_TO_POINTER (action_id)); + g_return_if_fail (action); + + g_debug ("Accelerator %d activated: %s", action_id, action); + + g_return_if_fail (g_action_group_has_action (G_ACTION_GROUP (self), action)); + g_signal_emit (self, signals[PRESSED], 0, action); + + if (g_action_group_get_action_parameter_type (G_ACTION_GROUP (self), action)) + pressed = g_variant_new_boolean (TRUE); + + g_action_group_activate_action (G_ACTION_GROUP (self), action, pressed); +} + + +static void +handle_accelerator_released_event (void *data, + struct phosh_private_keyboard_event *kbevent, + uint32_t action_id, + uint32_t timestamp) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (data); + const char *action; + + action = g_hash_table_lookup (self->accelerators, GUINT_TO_POINTER (action_id)); + g_return_if_fail (action); + + g_debug ("Accelerator %d released: %s", action_id, action); + + g_return_if_fail (g_action_group_has_action (G_ACTION_GROUP (self), action)); + g_signal_emit (self, signals[RELEASED], 0, action); + + /* Action doesn't have a parameter so we only notify press */ + if (g_action_group_get_action_parameter_type (G_ACTION_GROUP (self), action) == NULL) + return; + + g_action_group_activate_action (G_ACTION_GROUP (self), action, g_variant_new_boolean (FALSE)); +} + + +static void +handle_grab_failed_event (void *data, + struct phosh_private_keyboard_event *kbevent, + const char *accelerator, + uint32_t error) +{ + switch ((enum phosh_private_keyboard_event_error) error) { + case PHOSH_PRIVATE_KEYBOARD_EVENT_ERROR_ALREADY_SUBSCRIBED: + g_warning ("Already subscribed to accelerator %s", accelerator); + break; + case PHOSH_PRIVATE_KEYBOARD_EVENT_ERROR_INVALID_KEYSYM: + g_warning ("Accelerator %s not subscribeable", accelerator); + break; + case PHOSH_PRIVATE_KEYBOARD_EVENT_ERROR_MISC_ERROR: + case PHOSH_PRIVATE_KEYBOARD_EVENT_ERROR_INVALID_ARGUMENT: + default: + g_warning ("Unknown error %d trying to subscribe accelerator %s", error, accelerator); + } +} + + +static void +handle_grab_success_event (void *data, + struct phosh_private_keyboard_event *kbevent, + const char *accelerator, + uint32_t action_id) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (data); + + g_hash_table_insert (self->accelerators, GUINT_TO_POINTER (action_id), g_strdup (accelerator)); +} + + +static void +handle_ungrab_success_event (void *data, + struct phosh_private_keyboard_event *kbevent, + uint32_t action_id) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (data); + + g_return_if_fail (PHOSH_IS_KEYBOARD_EVENTS (data)); + g_debug ("Ungrab of %d successful", action_id); + g_hash_table_remove (self->accelerators, GUINT_TO_POINTER (action_id)); +} + + +static void +handle_ungrab_failed_event (void *data, + struct phosh_private_keyboard_event *kbevent, + uint32_t action_id, + uint32_t error) +{ + g_warning ("Ungrab of %d failed: %d", action_id, error); +} + + +static const struct phosh_private_keyboard_event_listener keyboard_event_listener = { + .accelerator_activated_event = handle_accelerator_activated_event, + .accelerator_released_event = handle_accelerator_released_event, + .grab_failed_event = handle_grab_failed_event, + .grab_success_event = handle_grab_success_event, + .ungrab_failed_event = handle_ungrab_failed_event, + .ungrab_success_event = handle_ungrab_success_event, +}; + + +static void +on_action_added (PhoshKeyboardEvents *self, + char *action_name, + GActionGroup *action_group) +{ + g_debug ("Grabbing accelerator %s", action_name); + phosh_private_keyboard_event_grab_accelerator_request (self->kbevent, action_name); +} + + +static void +on_action_removed (PhoshKeyboardEvents *self, + char *action_name, + GActionGroup *action_group) +{ + GHashTableIter iter; + gpointer key, value; + + g_debug ("Ungrabbing accelerator %s", action_name); + + g_hash_table_iter_init (&iter, self->accelerators); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if (!g_strcmp0 (action_name, value)) { + phosh_private_keyboard_event_ungrab_accelerator_request (self->kbevent, + GPOINTER_TO_UINT (key)); + } + } +} + + +static gboolean +initable_init (GInitable *initable, + GCancellable *cancelable, + GError **error) +{ + struct phosh_private *phosh_private; + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (initable); + + phosh_private = phosh_wayland_get_phosh_private ( + phosh_wayland_get_default ()); + + if (!phosh_private) { + g_warning ("Skipping grab manager due to missing phosh_private protocol extension"); + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Missing phosh_private protocol extension!"); + return FALSE; + } + + if ((self->kbevent = phosh_private_get_keyboard_event (phosh_private)) == NULL) { + g_warning ("Skipping grab manager because of an unknown phosh_private protocol error"); + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Unknown protocol error (Running out of memory?)"); + return FALSE; + } + + phosh_private_keyboard_event_add_listener (self->kbevent, &keyboard_event_listener, self); + + g_signal_connect (self, + "action-added", + G_CALLBACK (on_action_added), + NULL); + + g_signal_connect (self, + "action-removed", + G_CALLBACK (on_action_removed), + NULL); + + return TRUE; +} + + +static void +initable_iface_init (GInitableIface *iface) +{ + iface->init = initable_init; +} + + +static void +phosh_keyboard_events_dispose (GObject *object) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (object); + + g_clear_pointer (&self->kbevent, phosh_private_keyboard_event_destroy); + + G_OBJECT_CLASS (phosh_keyboard_events_parent_class)->dispose (object); +} + + +static void +phosh_keyboard_events_finalize (GObject *object) +{ + PhoshKeyboardEvents *self = PHOSH_KEYBOARD_EVENTS (object); + + g_clear_pointer (&self->accelerators, g_hash_table_unref); + + G_OBJECT_CLASS (phosh_keyboard_events_parent_class)->finalize (object); +} + + + +static void +phosh_keyboard_events_class_init (PhoshKeyboardEventsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_keyboard_events_dispose; + object_class->finalize = phosh_keyboard_events_finalize; + + /** + * PhoshKeyboardEvents::pressed: + * @self: The keyboard-events instance + * @combo: The pressed global shortcut as string + * + * Emitted when a subscribed key binding was pressed. + */ + signals[PRESSED] = g_signal_new ("pressed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_STRING); + /** + * PhoshKeyboardEvents::released: + * @self: The keyboard-events instance + * @combo: The released global shortcut as string + * + * Emitted when a subscribed key binding was released. + */ + signals[RELEASED] = g_signal_new ("released", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_STRING); +} + + +static void +phosh_keyboard_events_init (PhoshKeyboardEvents *self) +{ + self->accelerators = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + g_free); +} + + +PhoshKeyboardEvents * +phosh_keyboard_events_new (GError **err) +{ + return g_initable_new (PHOSH_TYPE_KEYBOARD_EVENTS, + NULL, + err, + NULL); +} diff --git a/src/keyboard-events.h b/src/keyboard-events.h new file mode 100644 index 000000000..f2fefc6c3 --- /dev/null +++ b/src/keyboard-events.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2020 Evangelos Ribeiro Tzaras + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Evangelos Ribeiro Tzaras + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_KEYBOARD_EVENTS (phosh_keyboard_events_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshKeyboardEvents, + phosh_keyboard_events, + PHOSH, + KEYBOARD_EVENTS, + GSimpleActionGroup) + +PhoshKeyboardEvents *phosh_keyboard_events_new (GError **err); + +G_END_DECLS diff --git a/src/keypad.c b/src/keypad.c new file mode 100644 index 000000000..2d6f3747c --- /dev/null +++ b/src/keypad.c @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * based on LGPL-2.1+ HdyKeypad which is + * Copyright (C) 2019 Purism SPC + */ + +#include "phosh-config.h" +#include + +#include "keypad.h" +#include "util.h" + +#include + +/** + * PhoshKeypad: + * + * A keypad for pin input + * + * The #PhoshKeypad widget mimics a physical keypad for entering + * PIN codes on e.g. a #PhoshLockscreen. It can randomly + * distribute (shuffle) the digits. + * + * # CSS nodes + * + * #PhoshKeypad has a single CSS node with name phosh-keypad. + */ + +#define NUM_DIGITS 10 +/* Positions of the buttons in the grid we shuffle as x,y coordinates */ +static int btn_pos[NUM_DIGITS][2] = { { 1, 3 }, + { 0, 0 }, { 1, 0 }, { 2, 0 }, + { 0, 1 }, { 1, 1 }, { 2, 1 }, + { 0, 2 }, { 1, 2 }, { 2, 2 }}; + +typedef struct _PhoshKeypad { + GtkGrid parent; + + GtkEntry *entry; + /* The digit buttinos 1..9 and 0 */ + GtkWidget *buttons[10]; + + gboolean shuffle; +} PhoshKeypad; + +G_DEFINE_TYPE (PhoshKeypad, phosh_keypad, GTK_TYPE_GRID) + +enum { + PROP_0, + PROP_ENTRY, + PROP_END_ACTION, + PROP_START_ACTION, + PROP_SHUFFLE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +static void +symbol_clicked (PhoshKeypad *self, char symbol) +{ + g_autofree char *string = g_strdup_printf ("%c", symbol); + + if (!self->entry) + return; + + g_signal_emit_by_name (self->entry, "insert-at-cursor", string, NULL); + /* Set focus to the entry only when it can get focus + * https://gitlab.gnome.org/GNOME/gtk/issues/2204 + */ + if (gtk_widget_get_can_focus (GTK_WIDGET (self->entry))) + gtk_entry_grab_focus_without_selecting (self->entry); +} + + +static void +on_button_clicked (PhoshKeypad *self, + GtkButton *btn) +{ + GtkWidget *label = gtk_bin_get_child (GTK_BIN (btn)); + const char *text = gtk_label_get_label (GTK_LABEL (label)); + + g_return_if_fail (!gm_str_is_null_or_empty (text)); + + symbol_clicked (self, text[0]); +} + + +static void +phosh_keypad_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshKeypad *self = PHOSH_KEYPAD (object); + + switch (property_id) { + case PROP_ENTRY: + phosh_keypad_set_entry (self, g_value_get_object (value)); + break; + case PROP_END_ACTION: + phosh_keypad_set_end_action (self, g_value_get_object (value)); + break; + case PROP_START_ACTION: + phosh_keypad_set_start_action (self, g_value_get_object (value)); + break; + case PROP_SHUFFLE: + phosh_keypad_set_shuffle (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_keypad_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshKeypad *self = PHOSH_KEYPAD (object); + + switch (property_id) { + case PROP_ENTRY: + g_value_set_object (value, phosh_keypad_get_entry (self)); + break; + case PROP_START_ACTION: + g_value_set_object (value, phosh_keypad_get_start_action (self)); + break; + case PROP_END_ACTION: + g_value_set_object (value, phosh_keypad_get_end_action (self)); + break; + case PROP_SHUFFLE: + g_value_set_boolean (value, phosh_keypad_get_shuffle(self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +swap_buttons (PhoshKeypad *self, int pos_a, int pos_b) +{ + GtkWidget *a, *b; + + int c_a = btn_pos[pos_a][0]; + int r_a = btn_pos[pos_a][1]; + int c_b = btn_pos[pos_b][0]; + int r_b = btn_pos[pos_b][1]; + + if (pos_a == pos_b) + return; + + a = gtk_grid_get_child_at (GTK_GRID (self), c_a, r_a); + gtk_container_remove (GTK_CONTAINER (self), a); + + b = gtk_grid_get_child_at (GTK_GRID (self), c_b, r_b); + gtk_container_remove (GTK_CONTAINER (self), b); + + gtk_grid_attach (GTK_GRID (self), a, c_b, r_b, 1, 1); + gtk_grid_attach (GTK_GRID (self), b, c_a, r_a, 1, 1); +} + + +static void +distribute_buttons (PhoshKeypad *self, gboolean shuffle) +{ + if (shuffle) { + /* Fisher-Yates shuffle */ + for (int i = 0; i < NUM_DIGITS-1; i++) { + int j = g_random_int_range (i, NUM_DIGITS); + swap_buttons (self, i, j); + } + } else { + /* Use sorted positions */ + for (int i = 0; i < NUM_DIGITS; i++) { + GtkWidget *old; + int c = btn_pos[i][0]; + int r = btn_pos[i][1]; + + old = gtk_grid_get_child_at (GTK_GRID (self), c, r); + gtk_container_remove (GTK_CONTAINER (self), old); + } + + for (int i = 0; i < NUM_DIGITS; i++) { + int c = btn_pos[i][0]; + int r = btn_pos[i][1]; + + gtk_grid_attach (GTK_GRID (self), self->buttons[i], c, r, 1, 1); + } + } +} + + +static void +phosh_keypad_dispose (GObject *object) +{ + PhoshKeypad *self = PHOSH_KEYPAD (object); + + for (int i = 0; i < NUM_DIGITS; i++) + g_clear_object (&self->buttons[i]); + + G_OBJECT_CLASS (phosh_keypad_parent_class)->dispose (object); +} + + +static void +phosh_keypad_class_init (PhoshKeypadClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_keypad_dispose; + object_class->set_property = phosh_keypad_set_property; + object_class->get_property = phosh_keypad_get_property; + + /** + * PhoshKeypad:entry: + * + * The entry widget connected to the keypad. See phosh_keypad_set_entry() for + * details. + */ + props[PROP_ENTRY] = + g_param_spec_object ("entry", + "Entry", + "The entry widget connected to the keypad", + GTK_TYPE_ENTRY, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * PhoshKeypad:end-action: + * + * The widget for the lower end corner of @self. + */ + props[PROP_END_ACTION] = + g_param_spec_object ("end-action", + "End action", + "The end action widget", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * PhoshKeypad:start-action: + * + * The widget for the lower start corner of @self. + */ + props[PROP_START_ACTION] = + g_param_spec_object ("start-action", + "Start action", + "The start action widget", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * PhoshKeypad:shuffle: + * + * Whether to shuffle digits. Setting this to %TRUE will make + * the digits appear at random locations on the keypad. + */ + props[PROP_SHUFFLE] = + g_param_spec_boolean ("shuffle", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/keypad.ui"); + + for (int i = 0; i < NUM_DIGITS; i++) { + g_autofree char *name = g_strdup_printf ("btn_%d", i); + gtk_widget_class_bind_template_child_full (widget_class, + name, + FALSE, + G_STRUCT_OFFSET (PhoshKeypad, buttons[i])); + } + + gtk_widget_class_bind_template_callback (widget_class, on_button_clicked); + + gtk_widget_class_set_accessible_role (widget_class, ATK_ROLE_DIAL); + gtk_widget_class_set_css_name (widget_class, "phosh-keypad"); +} + + +static void +phosh_keypad_init (PhoshKeypad *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + for (int i = 0; i < NUM_DIGITS; i++) { + /* We hold an extra reference since we remove buttons when reordering + them and so we don't need to bother then */ + g_object_ref (self->buttons[i]); + } + distribute_buttons (self, self->shuffle); + + gtk_widget_set_direction (GTK_WIDGET (self), GTK_TEXT_DIR_LTR); +} + + +/** + * phosh_keypad_new: + * + * Create a new #PhoshKeypad widget. + * + * Returns: the newly created #PhoshKeypad widget + */ +GtkWidget * +phosh_keypad_new (void) +{ + return g_object_new (PHOSH_TYPE_KEYPAD, NULL); +} + +/** + * phosh_keypad_set_entry: + * @self: a #PhoshKeypad + * @entry: (nullable): a #GtkEntry + * + * Binds @entry to @self and blocks any input which wouldn't be possible to type + * with with the keypad. + */ +void +phosh_keypad_set_entry (PhoshKeypad *self, + GtkEntry *entry) +{ + g_return_if_fail (PHOSH_IS_KEYPAD (self)); + g_return_if_fail (entry == NULL || GTK_IS_ENTRY (entry)); + + if (entry == self->entry) + return; + + g_clear_object (&self->entry); + + if (entry) { + self->entry = g_object_ref (entry); + + gtk_widget_set_visible (GTK_WIDGET (self->entry), TRUE); + /* Workaround: To keep the osk closed + * https://gitlab.gnome.org/GNOME/gtk/merge_requests/978#note_546576 */ + g_object_set (self->entry, "im-module", "gtk-im-context-none", NULL); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENTRY]); +} + + +/** + * phosh_keypad_get_entry: + * @self: a #PhoshKeypad + * + * Get the connected entry. See phosh_keypad_set_entry() for details. + * + * Returns: (transfer none): the set #GtkEntry or %NULL if no widget was set + */ +GtkEntry * +phosh_keypad_get_entry (PhoshKeypad *self) +{ + g_return_val_if_fail (PHOSH_IS_KEYPAD (self), NULL); + + return self->entry; +} + + +/** + * phosh_keypad_set_start_action: + * @self: a #PhoshKeypad + * @start_action: (nullable): the start action widget + * + * Sets the widget for the lower left corner of + * @self. + */ +void +phosh_keypad_set_start_action (PhoshKeypad *self, + GtkWidget *start_action) +{ + GtkWidget *old_widget; + + g_return_if_fail (PHOSH_IS_KEYPAD (self)); + g_return_if_fail (start_action == NULL || GTK_IS_WIDGET (start_action)); + + old_widget = gtk_grid_get_child_at (GTK_GRID (self), 0, 3); + + if (old_widget == start_action) + return; + + if (old_widget != NULL) + gtk_container_remove (GTK_CONTAINER (self), old_widget); + + if (start_action != NULL) + gtk_grid_attach (GTK_GRID (self), start_action, 0, 3, 1, 1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION]); +} + + +/** + * phosh_keypad_get_start_action: + * @self: a #PhoshKeypad + * + * Returns the widget for the lower left corner of + * @self. + * + * Returns: (transfer none) (nullable): the start action widget + */ +GtkWidget * +phosh_keypad_get_start_action (PhoshKeypad *self) +{ + g_return_val_if_fail (PHOSH_IS_KEYPAD (self), NULL); + + return gtk_grid_get_child_at (GTK_GRID (self), 0, 3); +} + + +/** + * phosh_keypad_set_end_action: + * @self: a #PhoshKeypad + * @end_action: (nullable): the end action widget + * + * Sets the widget for the lower right corner of + * @self. + */ +void +phosh_keypad_set_end_action (PhoshKeypad *self, + GtkWidget *end_action) +{ + GtkWidget *old_widget; + + g_return_if_fail (PHOSH_IS_KEYPAD (self)); + g_return_if_fail (end_action == NULL || GTK_IS_WIDGET (end_action)); + + old_widget = gtk_grid_get_child_at (GTK_GRID (self), 2, 3); + + if (old_widget == end_action) + return; + + if (old_widget != NULL) + gtk_container_remove (GTK_CONTAINER (self), old_widget); + + if (end_action != NULL) + gtk_grid_attach (GTK_GRID (self), end_action, 2, 3, 1, 1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION]); +} + + +/** + * phosh_keypad_get_end_action: + * @self: a #PhoshKeypad + * + * Returns the widget for the lower right corner of + * @self. + * + * Returns: (transfer none) (nullable): the end action widget + */ +GtkWidget * +phosh_keypad_get_end_action (PhoshKeypad *self) +{ + g_return_val_if_fail (PHOSH_IS_KEYPAD (self), NULL); + + return gtk_grid_get_child_at (GTK_GRID (self), 2, 3); +} + + +void +phosh_keypad_set_shuffle (PhoshKeypad *self, gboolean shuffle) +{ + g_return_if_fail (PHOSH_IS_KEYPAD (self)); + + if (self->shuffle == shuffle) + return; + + self->shuffle = shuffle; + distribute_buttons (self, shuffle); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHUFFLE]); +} + + +gboolean +phosh_keypad_get_shuffle (PhoshKeypad *self) +{ + g_return_val_if_fail (PHOSH_IS_KEYPAD (self), FALSE); + + return self->shuffle; +} + + +/** + * phosh_keypad_distribute: + * @self: a #PhoshKeypad + * + * Redistribute buttons on keypad. If %PhoshKeypad:shuffle is %TRUE buttons + * will be reshuffled otherwise they will be ordered. + **/ +void +phosh_keypad_distribute (PhoshKeypad *self) +{ + g_return_if_fail (PHOSH_IS_KEYPAD (self)); + + distribute_buttons (self, self->shuffle); +} diff --git a/src/keypad.h b/src/keypad.h new file mode 100644 index 000000000..624939788 --- /dev/null +++ b/src/keypad.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_KEYPAD (phosh_keypad_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshKeypad, phosh_keypad, PHOSH, KEYPAD, GtkGrid) + +GtkWidget *phosh_keypad_new (void); +void phosh_keypad_set_entry (PhoshKeypad *self, + GtkEntry *entry); +GtkEntry *phosh_keypad_get_entry (PhoshKeypad *self); +void phosh_keypad_set_start_action (PhoshKeypad *self, + GtkWidget *start_action); +GtkWidget *phosh_keypad_get_start_action (PhoshKeypad *self); +void phosh_keypad_set_end_action (PhoshKeypad *self, + GtkWidget *end_action); +GtkWidget *phosh_keypad_get_end_action (PhoshKeypad *self); +void phosh_keypad_set_shuffle (PhoshKeypad *self, + gboolean shuffle); +gboolean phosh_keypad_get_shuffle (PhoshKeypad *self); +void phosh_keypad_distribute (PhoshKeypad *self); +G_END_DECLS diff --git a/src/launcher-entry-manager.c b/src/launcher-entry-manager.c new file mode 100644 index 000000000..38a035ebf --- /dev/null +++ b/src/launcher-entry-manager.c @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-launcher-entry-manager" + +#include "idle-manager.h" +#include "launcher-entry-manager.h" +#include "phosh-marshalers.h" +#include "session-presence.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include + +/** + * PhoshLauncherEntryManager: + * + * Handles the launcher entry DBus API. See + * https://wiki.ubuntu.com/Unity/LauncherAPI + * + * We currently don't own the `com.canonical.Unity` DBus name which is used + * by clients to refresh their values as most clients don't seem to care. + */ + +enum { + INFO_UPDATED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct _PhoshLauncherEntryManager +{ + PhoshManager parent; + + int dbus_id; + GDBusConnection *session_bus; + + GCancellable *cancel; +} PhoshLauncherEntryManager; + +G_DEFINE_TYPE (PhoshLauncherEntryManager, phosh_launcher_entry_manager, PHOSH_TYPE_MANAGER); + + +static void +on_update (GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ +#define APP_URI_SCHEME "application://" + PhoshLauncherEntryManager *self = PHOSH_LAUNCHER_ENTRY_MANAGER (user_data); + const char *app_uri, *desktop_file; + g_autoptr (GVariant) properties = NULL; + + g_return_if_fail (g_strcmp0 (g_variant_get_type_string (parameters), "(sa{sv})") == 0); + + g_variant_get (parameters, "(&s@a{sv})", &app_uri, &properties); + + g_return_if_fail (g_str_has_prefix (app_uri, APP_URI_SCHEME)); + + desktop_file = &app_uri[strlen (APP_URI_SCHEME)]; + + g_debug ("%s: %s: %s", object_path, desktop_file, signal_name); + + g_signal_emit (self, signals[INFO_UPDATED], 0, desktop_file, properties); +#undef APP_URI_SCHEME +} + + +static void +on_bus_get_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshLauncherEntryManager *self; + g_autoptr (GError) err = NULL; + GDBusConnection *session_bus; + + session_bus = g_bus_get_finish (res, &err); + if (!session_bus) { + phosh_async_error_warn (err, "Failed to connect to session bus"); + return; + } + + self = PHOSH_LAUNCHER_ENTRY_MANAGER (user_data); + self->session_bus = session_bus; + + /* Listen for launcher entry signals */ + self->dbus_id = g_dbus_connection_signal_subscribe (self->session_bus, + NULL, + "com.canonical.Unity.LauncherEntry", + "Update", + NULL, + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_update, + self, NULL); +} + + +static void +phosh_launcher_entry_manager_idle_init (PhoshManager *manager) +{ + PhoshLauncherEntryManager *self = PHOSH_LAUNCHER_ENTRY_MANAGER (manager); + + g_bus_get (G_BUS_TYPE_SESSION, self->cancel, on_bus_get_finished, self); +} + + +static void +phosh_launcher_entry_manager_finalize (GObject *object) +{ + PhoshLauncherEntryManager *self = PHOSH_LAUNCHER_ENTRY_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (self->dbus_id) { + g_dbus_connection_signal_unsubscribe (self->session_bus, self->dbus_id); + self->dbus_id = 0; + } + g_clear_object (&self->session_bus); + + G_OBJECT_CLASS (phosh_launcher_entry_manager_parent_class)->finalize (object); +} + + +static void +phosh_launcher_entry_manager_class_init (PhoshLauncherEntryManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->finalize = phosh_launcher_entry_manager_finalize; + + manager_class->idle_init = phosh_launcher_entry_manager_idle_init; + + signals[INFO_UPDATED] = g_signal_new ("info-updated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + _phosh_marshal_VOID__STRING_VARIANT, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_VARIANT); + g_signal_set_va_marshaller (signals[INFO_UPDATED], + G_TYPE_FROM_CLASS (klass), + _phosh_marshal_VOID__STRING_VARIANTv); +} + + +static void +phosh_launcher_entry_manager_init (PhoshLauncherEntryManager *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshLauncherEntryManager * +phosh_launcher_entry_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_LAUNCHER_ENTRY_MANAGER, NULL); +} diff --git a/src/launcher-entry-manager.h b/src/launcher-entry-manager.h new file mode 100644 index 000000000..eb24626a1 --- /dev/null +++ b/src/launcher-entry-manager.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LAUNCHER_ENTRY_MANAGER (phosh_launcher_entry_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLauncherEntryManager, phosh_launcher_entry_manager, + PHOSH, LAUNCHER_ENTRY_MANAGER, PhoshManager) + +PhoshLauncherEntryManager *phosh_launcher_entry_manager_new (void); + +G_END_DECLS diff --git a/src/layersurface-priv.h b/src/layersurface-priv.h new file mode 100644 index 000000000..61669ce42 --- /dev/null +++ b/src/layersurface-priv.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "layersurface.h" +/* TODO: We use the enum constants from here, use glib-mkenums */ +#include "wlr-layer-shell-unstable-v1-client-protocol.h" + +G_BEGIN_DECLS + +GtkWidget *phosh_layer_surface_new (gpointer layer_shell, + gpointer wl_output); +struct zwlr_layer_surface_v1 *phosh_layer_surface_get_layer_surface(PhoshLayerSurface *self); +struct wl_surface *phosh_layer_surface_get_wl_surface(PhoshLayerSurface *self); +void phosh_layer_surface_set_size(PhoshLayerSurface *self, + int width, + int height); +void phosh_layer_surface_set_margins(PhoshLayerSurface *self, + int top, + int right, + int bottom, + int left); +void phosh_layer_surface_set_exclusive_zone(PhoshLayerSurface *self, + int zone); +void phosh_layer_surface_set_kbd_interactivity(PhoshLayerSurface *self, + gboolean interactivity); +guint32 phosh_layer_surface_get_layer (PhoshLayerSurface *self); +void phosh_layer_surface_set_layer (PhoshLayerSurface *self, + guint32 layer); +void phosh_layer_surface_wl_surface_commit (PhoshLayerSurface *self); +void phosh_layer_surface_get_margins (PhoshLayerSurface *self, + int *top, + int *right, + int *bottom, + int *left); +int phosh_layer_surface_get_configured_width (PhoshLayerSurface *self); +int phosh_layer_surface_get_configured_height (PhoshLayerSurface *self); +void phosh_layer_surface_set_alpha (PhoshLayerSurface *self, + double alpha); +void phosh_layer_surface_set_stacked_above (PhoshLayerSurface *self, + PhoshLayerSurface *target); +void phosh_layer_surface_set_stacked_below (PhoshLayerSurface *self, + PhoshLayerSurface *target); +gpointer phosh_layer_surface_get_wl_output (PhoshLayerSurface *self); + +G_END_DECLS diff --git a/src/layersurface.c b/src/layersurface.c new file mode 100644 index 000000000..52885b795 --- /dev/null +++ b/src/layersurface.c @@ -0,0 +1,1015 @@ +/* + * Copyright (C) 2018-2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-layer-surface" + +#include "phosh-config.h" +#include "layersurface-priv.h" +#include "phosh-wayland.h" +#include "phoc-layer-shell-effects-unstable-v1-client-protocol.h" + +#include + +/** + * PhoshLayerSurface: + * + * A #GtkWindow rendered as a LayerSurface by the compositor + * + * #PhoshLayerSurface allows to use a Wayland surface backed by the + * layer-shell protocol as #GtkWindow. This allows to render e.g. panels and + * backgrounds using GTK. + */ + +enum { + PHOSH_LAYER_SURFACE_PROP_0, + PHOSH_LAYER_SURFACE_PROP_LAYER_SHELL, + PHOSH_LAYER_SURFACE_PROP_WL_OUTPUT, + PHOSH_LAYER_SURFACE_PROP_ANCHOR, + PHOSH_LAYER_SURFACE_PROP_LAYER, + PHOSH_LAYER_SURFACE_PROP_KBD_INTERACTIVITY, + PHOSH_LAYER_SURFACE_PROP_EXCLUSIVE_ZONE, + PHOSH_LAYER_SURFACE_PROP_MARGIN_TOP, + PHOSH_LAYER_SURFACE_PROP_MARGIN_BOTTOM, + PHOSH_LAYER_SURFACE_PROP_MARGIN_LEFT, + PHOSH_LAYER_SURFACE_PROP_MARGIN_RIGHT, + PHOSH_LAYER_SURFACE_PROP_LAYER_WIDTH, + PHOSH_LAYER_SURFACE_PROP_LAYER_HEIGHT, + PHOSH_LAYER_SURFACE_PROP_CONFIGURED_WIDTH, + PHOSH_LAYER_SURFACE_PROP_CONFIGURED_HEIGHT, + PHOSH_LAYER_SURFACE_PROP_NAMESPACE, + PHOSH_LAYER_SURFACE_PROP_LAST_PROP +}; +static GParamSpec *props[PHOSH_LAYER_SURFACE_PROP_LAST_PROP]; + +enum { + CONFIGURED, + N_SIGNALS +}; +static guint signals [N_SIGNALS]; + +typedef struct { + struct wl_surface *wl_surface; + struct zwlr_layer_surface_v1 *layer_surface; + struct zphoc_alpha_layer_surface_v1 *alpha_surface; + struct zphoc_stacked_layer_surface_v1 *stacked_surface; + + /* Properties */ + guint anchor; + guint layer; + gboolean kbd_interactivity; + int exclusive_zone; + int margin_top, margin_bottom; + int margin_left, margin_right; + int width, height; + int configured_width, configured_height; + char *namespace; + struct zwlr_layer_shell_v1 *layer_shell; + struct wl_output *wl_output; + /* alpha_layer_surface_v1 */ + double alpha; + /* stacked_layer_surface_v1 */ + PhoshLayerSurface *stack_target; + gboolean stack_above; +} PhoshLayerSurfacePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshLayerSurface, phosh_layer_surface, GTK_TYPE_WINDOW) + + +static void +layer_surface_configure (void *data, + struct zwlr_layer_surface_v1 *surface, + uint32_t serial, + uint32_t width, + uint32_t height) +{ + PhoshLayerSurface *self = data; + PhoshLayerSurfacePrivate *priv; + gboolean changed = FALSE; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + gtk_window_resize (GTK_WINDOW (self), width, height); + zwlr_layer_surface_v1_ack_configure (surface, serial); + + if (priv->configured_height != height) { + priv->configured_height = height; + changed = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_CONFIGURED_HEIGHT]); + } + + if (priv->configured_width != width) { + priv->configured_width = width; + changed = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_CONFIGURED_WIDTH]); + } + + g_debug ("Configured '%s' (%p) (%dx%d)", priv->namespace, self, width, height); + if (changed) + g_signal_emit (self, signals[CONFIGURED], 0); +} + + +static void +layer_surface_closed (void *data, + struct zwlr_layer_surface_v1 *surface) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (data); + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + g_return_if_fail (priv->layer_surface == surface); + g_debug ("Destroying layer surface '%s' (%p)", priv->namespace, self); + zwlr_layer_surface_v1_destroy (priv->layer_surface); + priv->layer_surface = NULL; + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +static struct zwlr_layer_surface_v1_listener layer_surface_listener = { + .configure = layer_surface_configure, + .closed = layer_surface_closed, +}; + + +static void +set_alpha (PhoshLayerSurface *self, double alpha) +{ + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + + priv->alpha = alpha; + + if (!priv->alpha_surface) + return; + + zphoc_alpha_layer_surface_v1_set_alpha (priv->alpha_surface, wl_fixed_from_double (alpha)); + wl_surface_commit (priv->wl_surface); +} + + +static void +phosh_layer_surface_set_stacked (PhoshLayerSurface *self, PhoshLayerSurface *target, gboolean above) +{ + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + PhoshLayerSurfacePrivate *target_priv = phosh_layer_surface_get_instance_private (target); + + if (priv->stack_target) + g_object_remove_weak_pointer (G_OBJECT (priv->stack_target), + (gpointer *)&priv->stack_target); + + priv->stack_target = target; + + if (priv->stack_target) { + g_object_add_weak_pointer (G_OBJECT (priv->stack_target), + (gpointer *)&priv->stack_target); + } else { + /* There's no "remove stacking" so we're done */ + return; + } + + if (priv->stacked_surface == NULL) { + g_debug ("Trying to stack an unmapped layer surface '%s'", priv->namespace); + return; + } + + if (target_priv->stacked_surface == NULL) { + g_debug ("Trying to stack above an unmapped layer surface '%s'", target_priv->namespace); + return; + } + + if (above) + zphoc_stacked_layer_surface_v1_stack_above (priv->stacked_surface, target_priv->layer_surface); + else + zphoc_stacked_layer_surface_v1_stack_below (priv->stacked_surface, target_priv->layer_surface); + + wl_surface_commit (priv->wl_surface); +} + + +static void +phosh_layer_surface_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (object); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + int width, height; + + switch (property_id) { + case PHOSH_LAYER_SURFACE_PROP_LAYER_SHELL: + priv->layer_shell = g_value_get_pointer (value); + break; + case PHOSH_LAYER_SURFACE_PROP_WL_OUTPUT: + priv->wl_output = g_value_get_pointer (value); + break; + case PHOSH_LAYER_SURFACE_PROP_ANCHOR: + priv->anchor = g_value_get_uint (value); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER: + phosh_layer_surface_set_layer (self, g_value_get_uint (value)); + break; + case PHOSH_LAYER_SURFACE_PROP_KBD_INTERACTIVITY: + phosh_layer_surface_set_kbd_interactivity (self, g_value_get_boolean (value)); + break; + case PHOSH_LAYER_SURFACE_PROP_EXCLUSIVE_ZONE: + phosh_layer_surface_set_exclusive_zone (self, g_value_get_int (value)); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_TOP: + phosh_layer_surface_set_margins (self, + g_value_get_int (value), + priv->margin_right, + priv->margin_bottom, + priv->margin_left); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_BOTTOM: + phosh_layer_surface_set_margins (self, + priv->margin_top, + priv->margin_right, + g_value_get_int (value), + priv->margin_left); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_LEFT: + phosh_layer_surface_set_margins (self, + priv->margin_top, + priv->margin_right, + priv->margin_bottom, + g_value_get_int (value)); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_RIGHT: + phosh_layer_surface_set_margins (self, + priv->margin_top, + g_value_get_int (value), + priv->margin_bottom, + priv->margin_left); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER_WIDTH: + width = g_value_get_uint (value); + phosh_layer_surface_set_size (self, width, priv->height); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER_HEIGHT: + height = g_value_get_uint (value); + phosh_layer_surface_set_size (self, priv->width, height); + break; + case PHOSH_LAYER_SURFACE_PROP_NAMESPACE: + g_free (priv->namespace); + priv->namespace = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_layer_surface_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (object); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + + switch (property_id) { + case PHOSH_LAYER_SURFACE_PROP_LAYER_SHELL: + g_value_set_pointer (value, priv->layer_shell); + break; + case PHOSH_LAYER_SURFACE_PROP_WL_OUTPUT: + g_value_set_pointer (value, priv->wl_output); + break; + case PHOSH_LAYER_SURFACE_PROP_ANCHOR: + g_value_set_uint (value, priv->anchor); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER: + g_value_set_uint (value, phosh_layer_surface_get_layer (self)); + break; + case PHOSH_LAYER_SURFACE_PROP_KBD_INTERACTIVITY: + g_value_set_boolean (value, priv->kbd_interactivity); + break; + case PHOSH_LAYER_SURFACE_PROP_EXCLUSIVE_ZONE: + g_value_set_int (value, priv->exclusive_zone); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_TOP: + g_value_set_int (value, priv->margin_top); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_BOTTOM: + g_value_set_int (value, priv->margin_bottom); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_LEFT: + g_value_set_int (value, priv->margin_left); + break; + case PHOSH_LAYER_SURFACE_PROP_MARGIN_RIGHT: + g_value_set_int (value, priv->margin_right); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER_WIDTH: + g_value_set_uint (value, priv->width); + break; + case PHOSH_LAYER_SURFACE_PROP_LAYER_HEIGHT: + g_value_set_uint (value, priv->height); + break; + case PHOSH_LAYER_SURFACE_PROP_CONFIGURED_WIDTH: + g_value_set_uint (value, priv->configured_width); + break; + case PHOSH_LAYER_SURFACE_PROP_CONFIGURED_HEIGHT: + g_value_set_uint (value, priv->configured_height); + break; + case PHOSH_LAYER_SURFACE_PROP_NAMESPACE: + g_value_set_string (value, priv->namespace); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_layer_surface_realize (GtkWidget *widget) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (widget); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + GdkWindow *gdk_window; + + GTK_WIDGET_CLASS (phosh_layer_surface_parent_class)->realize (widget); + + gdk_window = gtk_widget_get_window (GTK_WIDGET (self)); + gdk_wayland_window_set_use_custom_surface (gdk_window); + priv->wl_surface = gdk_wayland_window_get_wl_surface (gdk_window); + + gtk_window_set_decorated (GTK_WINDOW (self), FALSE); +} + + +static void +phosh_layer_surface_map (GtkWidget *widget) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (widget); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + PhoshWayland *wl = phosh_wayland_get_default (); + struct zphoc_layer_shell_effects_v1 *layer_shell_effects; + + GTK_WIDGET_CLASS (phosh_layer_surface_parent_class)->map (widget); + + if (!priv->wl_surface) { + GdkWindow *gdk_window; + + gdk_window = gtk_widget_get_window (GTK_WIDGET (self)); + gdk_wayland_window_set_use_custom_surface (gdk_window); + priv->wl_surface = gdk_wayland_window_get_wl_surface (gdk_window); + } + g_debug ("Mapped '%s' (%p)", priv->namespace, self); + + priv->layer_surface = zwlr_layer_shell_v1_get_layer_surface (priv->layer_shell, + priv->wl_surface, + priv->wl_output, + priv->layer, + priv->namespace); + zwlr_layer_surface_v1_set_exclusive_zone (priv->layer_surface, priv->exclusive_zone); + zwlr_layer_surface_v1_set_size (priv->layer_surface, priv->width, priv->height); + zwlr_layer_surface_v1_set_anchor (priv->layer_surface, priv->anchor); + zwlr_layer_surface_v1_set_margin (priv->layer_surface, + priv->margin_top, + priv->margin_right, + priv->margin_bottom, + priv->margin_left); + zwlr_layer_surface_v1_set_keyboard_interactivity (priv->layer_surface, priv->kbd_interactivity); + zwlr_layer_surface_v1_add_listener (priv->layer_surface, + &layer_surface_listener, + self); + wl_surface_commit (priv->wl_surface); + + /* Process all pending events, otherwise we end up sending ack configure + * to a not yet configured surface */ + wl_display_roundtrip (gdk_wayland_display_get_wl_display (gdk_display_get_default ())); + + layer_shell_effects = phosh_wayland_get_zphoc_layer_shell_effects_v1 (wl); + priv->alpha_surface = + zphoc_layer_shell_effects_v1_get_alpha_layer_surface (layer_shell_effects, + priv->layer_surface); + priv->stacked_surface = + zphoc_layer_shell_effects_v1_get_stacked_layer_surface (layer_shell_effects, + priv->layer_surface); + + /* Catch up with alpha values set before map */ + if (!G_APPROX_VALUE (priv->alpha, 1.0, FLT_EPSILON)) + set_alpha (self, priv->alpha); + + /* Catch up with stackings set before map */ + if (priv->stacked_surface) + phosh_layer_surface_set_stacked (self, priv->stack_target, priv->stack_above); +} + + +static void +phosh_layer_surface_unmap (GtkWidget *widget) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (widget); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + + g_clear_pointer (&priv->alpha_surface, zphoc_alpha_layer_surface_v1_destroy); + g_clear_pointer (&priv->stacked_surface, zphoc_stacked_layer_surface_v1_destroy); + g_clear_pointer (&priv->layer_surface, zwlr_layer_surface_v1_destroy); + priv->wl_surface = NULL; + + GTK_WIDGET_CLASS (phosh_layer_surface_parent_class)->unmap (widget); +} + + +static void +phosh_layer_surface_configured_impl (PhoshLayerSurface *layer_surface) +{ + /* Nothing todo here */ +} + + +static void +phosh_layer_surface_dispose (GObject *object) +{ + PhoshLayerSurface *self = PHOSH_LAYER_SURFACE (object); + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + + phosh_layer_surface_set_stacked (self, NULL, FALSE); + g_clear_pointer (&priv->alpha_surface, zphoc_alpha_layer_surface_v1_destroy); + g_clear_pointer (&priv->stacked_surface, zphoc_stacked_layer_surface_v1_destroy); + g_clear_pointer (&priv->layer_surface, zwlr_layer_surface_v1_destroy); + g_clear_pointer (&priv->namespace, g_free); + + G_OBJECT_CLASS (phosh_layer_surface_parent_class)->dispose (object); +} + + +static void +phosh_layer_surface_class_init (PhoshLayerSurfaceClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = (GtkWidgetClass *)klass; + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + + object_class->dispose = phosh_layer_surface_dispose; + object_class->set_property = phosh_layer_surface_set_property; + object_class->get_property = phosh_layer_surface_get_property; + + widget_class->realize = phosh_layer_surface_realize; + widget_class->map = phosh_layer_surface_map; + widget_class->unmap = phosh_layer_surface_unmap; + + layer_surface_class->configured = phosh_layer_surface_configured_impl; + + props[PHOSH_LAYER_SURFACE_PROP_LAYER_SHELL] = + g_param_spec_pointer ( + "layer-shell", + "Wayland Layer Shell Global", + "The layer shell wayland global", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PHOSH_LAYER_SURFACE_PROP_WL_OUTPUT] = + g_param_spec_pointer ( + "wl-output", + "Wayland Output", + "The wl_output associated with this surface", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PHOSH_LAYER_SURFACE_PROP_ANCHOR] = + g_param_spec_uint ( + "anchor", + "Anchor edges", + "The edges to anchor the surface to", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PHOSH_LAYER_SURFACE_PROP_LAYER] = + g_param_spec_uint ( + "layer", + "Layer", + "The layer the surface should be attached to", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_KBD_INTERACTIVITY] = + g_param_spec_boolean ( + "kbd-interactivity", + "Keyboard interactivity", + "Whether the surface interacts with the keyboard", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_EXCLUSIVE_ZONE] = + g_param_spec_int ( + "exclusive-zone", + "Exclusive Zone", + "Set area that is not occluded with other surfaces", + -1, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_MARGIN_LEFT] = + g_param_spec_int ( + "margin-left", + "Left margin", + "Distance away from the left anchor point", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_MARGIN_RIGHT] = + g_param_spec_int ( + "margin-right", + "Right margin", + "Distance away from the right anchor point", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_MARGIN_TOP] = + g_param_spec_int ( + "margin-top", + "Top margin", + "Distance away from the top anchor point", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_MARGIN_BOTTOM] = + g_param_spec_int ( + "margin-bottom", + "Bottom margin", + "Distance away from the bottom anchor point", + G_MININT, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_LAYER_WIDTH] = + g_param_spec_uint ( + "width", + "Width", + "The width of the layer surface", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_LAYER_HEIGHT] = + g_param_spec_uint ( + "height", + "Height", + "The height of the layer surface", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + + props[PHOSH_LAYER_SURFACE_PROP_CONFIGURED_WIDTH] = + g_param_spec_uint ( + "configured-width", + "Configured width", + "The width of the layer surface set by the compositor", + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_CONFIGURED_HEIGHT] = + g_param_spec_uint ( + "configured-height", + "Configured height", + "The height of the layer surface set by the compositor", + 0, + G_MAXUINT, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PHOSH_LAYER_SURFACE_PROP_NAMESPACE] = + g_param_spec_string ( + "namespace", + "Namespace", + "Namespace of the layer surface", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PHOSH_LAYER_SURFACE_PROP_LAST_PROP, props); + + /** + * PhoshLayerSurface::configured + * @self: The #PhoshLayerSurface instance. + * + * This signal is emitted once we received the configure event from the + * compositor. + */ + signals[CONFIGURED] = + g_signal_new ("configured", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (PhoshLayerSurfaceClass, configured), + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + + +static void +phosh_layer_surface_init (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv = phosh_layer_surface_get_instance_private (self); + + priv->alpha = 1.0; +} + + +GtkWidget * +phosh_layer_surface_new (gpointer layer_shell, + gpointer wl_output) +{ + return g_object_new (PHOSH_TYPE_LAYER_SURFACE, + "layer-shell", layer_shell, + "wl-output", wl_output, + NULL); +} + + +/** + * phosh_layer_surface_get_surface: + * @self: The #PhoshLayerSurface + * + * Get the layer layer surface or #NULL if the window + * is not yet realized. + */ +struct zwlr_layer_surface_v1 * +phosh_layer_surface_get_layer_surface (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), NULL); + priv = phosh_layer_surface_get_instance_private (self); + return priv->layer_surface; +} + + +/** + * phosh_layer_surface_get_wl_surface: + * @self: The #PhoshLayerSurface + * + * Get the layer wayland surface or #NULL if the window + * is not yet realized. + */ +struct wl_surface * +phosh_layer_surface_get_wl_surface (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), NULL); + priv = phosh_layer_surface_get_instance_private (self); + return priv->wl_surface; +} + + +/** + * phosh_layer_surface_set_size: + * @self: The #PhoshLayerSurface + * @width: the height in pixels + * @height: the width in pixels + * + * Set the size of a layer surface. A value of '-1' indicates 'use old value' + */ +void +phosh_layer_surface_set_size (PhoshLayerSurface *self, int width, int height) +{ + PhoshLayerSurfacePrivate *priv; + int old_width, old_height; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->height == height && priv->width == width) + return; + + old_width = priv->width; + old_height = priv->height; + + if (width != -1) + priv->width = width; + + if (height != -1) + priv->height = height; + + if (gtk_widget_get_mapped (GTK_WIDGET (self))) { + zwlr_layer_surface_v1_set_size (priv->layer_surface, priv->width, priv->height); + } + + if (priv->height != old_height) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_LAYER_HEIGHT]); + + if (priv->width != old_width) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_LAYER_WIDTH]); +} + + +/** + * phosh_layer_surface_set_margins: + * @self: The #PhoshLayerSurface + * @top: the top margin in pixels + * @right: the right margin in pixels + * @bottom: the bottom margin in pixels + * @left: the left margin in pixels + * + * Set anchor margins of a layer surface. + */ +void +phosh_layer_surface_set_margins (PhoshLayerSurface *self, int top, int right, int bottom, int left) +{ + PhoshLayerSurfacePrivate *priv; + int old_top, old_bottom, old_left, old_right; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + old_top = priv->margin_top; + old_left = priv->margin_left; + old_right = priv->margin_right; + old_bottom = priv->margin_bottom; + + if (old_top == top && old_left == left && old_right == right && old_bottom == bottom) + return; + + priv->margin_top = top; + priv->margin_left = left; + priv->margin_right = right; + priv->margin_bottom = bottom; + + if (priv->layer_surface) + zwlr_layer_surface_v1_set_margin (priv->layer_surface, top, right, bottom, left); + + if (old_top != top) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_MARGIN_TOP]); + if (old_bottom != bottom) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_MARGIN_BOTTOM]); + if (old_left != left) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_MARGIN_LEFT]); + if (old_right != right) + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_MARGIN_RIGHT]); +} + + +/** + * phosh_layer_surface_set_exclusive_zone: + * @self: The #PhoshLayerSurface + * @zone: Size of the exclusive zone. + * + * Set exclusive zone of a layer surface. + */ +void +phosh_layer_surface_set_exclusive_zone (PhoshLayerSurface *self, int zone) +{ + PhoshLayerSurfacePrivate *priv; + int old_zone; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + old_zone = priv->exclusive_zone; + + if (old_zone == zone) + return; + + priv->exclusive_zone = zone; + + if (priv->layer_surface) + zwlr_layer_surface_v1_set_exclusive_zone (priv->layer_surface, zone); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_EXCLUSIVE_ZONE]); +} + + +/** + * phosh_layer_surface_set_keyboard_interactivity: + * @self: The #PhoshLayerSurface + * @interactivity: %TRUE if the #PhoshLayerSurface should receive keyboard input. + * + * Set keyboard ineractivity a layer surface. + */ +void +phosh_layer_surface_set_kbd_interactivity (PhoshLayerSurface *self, gboolean interactivity) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->kbd_interactivity == interactivity) + return; + + priv->kbd_interactivity = interactivity; + + if (priv->layer_surface) + zwlr_layer_surface_v1_set_keyboard_interactivity (priv->layer_surface, interactivity); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_KBD_INTERACTIVITY]); +} + +/** + * phosh_layer_surface_get_layer: + * @self: The #PhoshLayerSurface + * + * Gets the surfaces current layer. + * + * Returns: The layer + */ +guint32 +phosh_layer_surface_get_layer (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), 0); + priv = phosh_layer_surface_get_instance_private (self); + + return priv->layer; +} + +/** + * phosh_layer_surface_set_layer: + * @self: The #PhoshLayerSurface + * @layer: The layer. + * + * Sets the layer a layer-surface belongs to `layer`. + */ +void +phosh_layer_surface_set_layer (PhoshLayerSurface *self, guint32 layer) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->layer == layer) + return; + + priv->layer = layer; + + if (priv->layer_surface) + zwlr_layer_surface_v1_set_layer (priv->layer_surface, layer); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_LAYER_SURFACE_PROP_LAYER]); +} + +/** + * phosh_layer_surface_get_wl_output: + * @self: The #PhoshLayerSurface + * + * Gets the output. + * + * Returns:(nullable)(transfer none): The output + */ +gpointer +phosh_layer_surface_get_wl_output (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), 0); + priv = phosh_layer_surface_get_instance_private (self); + + return priv->wl_output; +} + +/** + * phosh_layer_surface_wl_surface_commit: + * @self: The #PhoshLayerSurface + * + * Forces a commit of layer surface's state. + */ +void +phosh_layer_surface_wl_surface_commit (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->wl_surface) + wl_surface_commit (priv->wl_surface); +} + + +void +phosh_layer_surface_get_margins (PhoshLayerSurface *self, int *top, int *right, int *bottom, int *left) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + if (top) + *top = priv->margin_top; + + if (right) + *right = priv->margin_right; + + if (bottom) + *bottom = priv->margin_bottom; + + if (left) + *left = priv->margin_left; +} + + +int +phosh_layer_surface_get_configured_width (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), 0); + priv = phosh_layer_surface_get_instance_private (self); + + return priv->configured_width; +} + +int +phosh_layer_surface_get_configured_height (PhoshLayerSurface *self) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LAYER_SURFACE (self), 0); + priv = phosh_layer_surface_get_instance_private (self); + + return priv->configured_height; +} + + +void +phosh_layer_surface_set_alpha (PhoshLayerSurface *self, double alpha) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + priv = phosh_layer_surface_get_instance_private (self); + + g_return_if_fail (alpha >= 0.0 && alpha <= 1.0); + + if (G_APPROX_VALUE (priv->alpha, alpha, FLT_EPSILON)) + return; + + set_alpha (self, alpha); +} + +/** + * phosh_layer_surface_set_stacked_above: + * @self: The surface to be stacked + * @target: The target surface + * + * Stacks the surface `self` directly above `target`. + */ +void +phosh_layer_surface_set_stacked_above (PhoshLayerSurface *self, PhoshLayerSurface *target) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (target)); + + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->stack_target == target && priv->stack_above == TRUE) + return; + + phosh_layer_surface_set_stacked (self, target, TRUE); +} + +/** + * phosh_layer_surface_set_stacked_below: + * @self: The surface to be stacked + * @target: The target surface + * + * Stacks the surface `self` directly below `target`. + */ +void +phosh_layer_surface_set_stacked_below (PhoshLayerSurface *self, PhoshLayerSurface *target) +{ + PhoshLayerSurfacePrivate *priv; + + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (self)); + g_return_if_fail (PHOSH_IS_LAYER_SURFACE (target)); + + priv = phosh_layer_surface_get_instance_private (self); + + if (priv->stack_target == target && priv->stack_above == FALSE) + return; + + phosh_layer_surface_set_stacked (self, target, FALSE); +} diff --git a/src/layersurface.h b/src/layersurface.h new file mode 100644 index 000000000..dd1a31367 --- /dev/null +++ b/src/layersurface.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LAYER_SURFACE (phosh_layer_surface_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshLayerSurface, phosh_layer_surface, PHOSH, LAYER_SURFACE, GtkWindow) + +/** + * PhoshLayerSurfaceClass + * @parent_class: The parent class + * @configured: invoked when layer surface is configured + */ +struct _PhoshLayerSurfaceClass +{ + GtkWindowClass parent_class; + + /* Signals + */ + void (*configured) (PhoshLayerSurface *self); + + /* Padding for future expansion */ + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + +G_END_DECLS diff --git a/src/layout-manager.c b/src/layout-manager.c new file mode 100644 index 000000000..e50a65a4d --- /dev/null +++ b/src/layout-manager.c @@ -0,0 +1,481 @@ +/* + * Copyright (C) 2023-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-layout-manager" + +#include "phosh-config.h" + +#include "layout-manager.h" +#include "monitor/monitor.h" +#include "shell-priv.h" +#include "top-panel.h" + +#include +#include + +#define SHELL_LAYOUT_KEY "shell-layout" + +enum { + LAYOUT_CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +/** + * PhoshLayoutManager: + * + * Provide information on how to layout shell elements. + * + * Calculations are done in GTKs coordinates (thus scaled). + * + * Since: 0.29.0 + */ + +struct _PhoshLayoutManager { + GObject parent; + + GmDisplayPanel *panel; + GSettings *settings; + PhoshLayoutClockPosition clock_pos; + guint clock_shift; + guint corner_shift; + guint network_box_shift; + guint indicators_box_shift; + + PhoshMonitor *builtin; +}; +G_DEFINE_TYPE (PhoshLayoutManager, phosh_layout_manager, G_TYPE_OBJECT) + +/* Width of an item in the indicator box */ +#define BOX_ITEM_WIDTH 24 /* px */ + +/* The minimum space needed when shifting the indicator area due to a + * cutout. This is less than the `*_box_rect.width` as the clock will + * just be shifted by GTK in those cases */ +#define CORNER_CUTOUT_SHIFT_THRESHOLD (PHOSH_TOP_BAR_MIN_PADDING + 3 * BOX_ITEM_WIDTH) + +/* Space we want reserved for the central clock */ +static const GdkRectangle center_clock_rect = { + .width = 40, + .height = PHOSH_TOP_BAR_HEIGHT +}; + +static const GdkRectangle network_box_rect = { + /* Relevant icons are wifi, bt, 2 * wwan, network status */ + .width = BOX_ITEM_WIDTH * 5, + .height = PHOSH_TOP_BAR_HEIGHT +}; + +static const GdkRectangle indicators_box_rect = { + /* Relevant icons are battery, vpn, location and language */ + .width = BOX_ITEM_WIDTH * 4 /* max icons */, + .height = PHOSH_TOP_BAR_HEIGHT +}; + +/** + * get_clock_pos: + * @self: The layout manager + * @clock_shift:(out): How far to shift the settings clock downwards + * + * Update the clock's positions. + * + * If the clock can't be in the center due to the built-in display + * having a notch shift it left or right (whatever causes less overlap + * with the notch) + * + * We assume the panel uses it's native orientation. + * + * Returns: The desired clock position + */ +static PhoshLayoutClockPosition +get_clock_pos (PhoshLayoutManager *self, guint *clock_shift) +{ + PhoshLayoutClockPosition clock_pos = PHOSH_LAYOUT_CLOCK_POS_CENTER; + PhoshShellLayout layout; + GListModel *cutouts; + GdkRectangle clock_rect; + float scale; + guint shift = 0; + + layout = g_settings_get_enum (self->settings, SHELL_LAYOUT_KEY); + if (layout != PHOSH_SHELL_LAYOUT_DEVICE) + return clock_pos; + + if (self->builtin == NULL) + return clock_pos; + + if (phosh_monitor_get_transform (self->builtin) != PHOSH_MONITOR_TRANSFORM_NORMAL) { + /* TODO: we can do better here */ + return clock_pos; + } + + scale = phosh_monitor_get_fractional_scale (self->builtin); + clock_rect = center_clock_rect; + clock_rect.x = (self->builtin->width / scale / 2.0) - center_clock_rect.width / 2.0; + + cutouts = gm_display_panel_get_cutouts (self->panel); + for (int i = 0; i < g_list_model_get_n_items (cutouts); i++) { + g_autoptr (GmCutout) cutout = g_list_model_get_item (cutouts, i); + GdkRectangle notch; + int overlap_left, overlap_right; + const GmRect *bounds; + + bounds = gm_cutout_get_bounds (cutout); + notch = (GdkRectangle) { + .x = floor (bounds->x / scale), + .y = floor (bounds->y / scale), + .width = ceil (bounds->width / scale), + .height = ceil (bounds->height / scale), + }; + + /* Look for top-bar notch */ + if (!gdk_rectangle_intersect (¬ch, &clock_rect, NULL)) + continue; + shift = notch.height + notch.y; + + overlap_left = network_box_rect.width + center_clock_rect.width - notch.x; + if (overlap_left <= 0) { + clock_pos = PHOSH_LAYOUT_CLOCK_POS_LEFT; + break; + } + + overlap_right = (indicators_box_rect.width + center_clock_rect.width - + ((self->builtin->width / scale) - notch.x - notch.width)); + if (overlap_right <= 0) { + clock_pos = PHOSH_LAYOUT_CLOCK_POS_RIGHT; + break; + } + + g_debug ("Notch overlaps left: %d, right: %d", overlap_left, overlap_right); + g_warning ("No clock placement found to fully avoid notch"); + clock_pos = (overlap_left < overlap_right) ? PHOSH_LAYOUT_CLOCK_POS_LEFT : + PHOSH_LAYOUT_CLOCK_POS_RIGHT; + break; + } + + if (clock_shift) + *clock_shift = shift; + + g_debug ("Center clock pos: %d, margin: %u", clock_pos, shift); + return clock_pos; +} + +/** + * get_cutout_shifts: + * @self: The layout manager + * + * Get the shifts for network and indicator box. + * + * If the network box can't be left aligned due to the built-in + * display having a notch shift it to the right. Similar for the + * indicators but shift them to the left. + * + * We assume the panel uses its native orientation. + */ +static void +get_cutout_shifts (PhoshLayoutManager *self, guint *network, guint *indicators) +{ + PhoshShellLayout layout; + GListModel *cutouts; + float scale; + GdkRectangle indicators_box = indicators_box_rect; + GdkRectangle network_box = network_box_rect; + guint width; + + layout = g_settings_get_enum (self->settings, SHELL_LAYOUT_KEY); + if (layout != PHOSH_SHELL_LAYOUT_DEVICE) + goto out; + + if (self->builtin == NULL) + goto out; + + if (phosh_monitor_get_transform (self->builtin) != PHOSH_MONITOR_TRANSFORM_NORMAL) + goto out; + + scale = phosh_monitor_get_fractional_scale (self->builtin); + width = self->builtin->logical.width; + + network_box.x = PHOSH_TOP_BAR_MIN_PADDING; + indicators_box.x = width - indicators_box.width - PHOSH_TOP_BAR_MIN_PADDING; + + cutouts = gm_display_panel_get_cutouts (self->panel); + for (int i = 0; i < g_list_model_get_n_items (cutouts); i++) { + g_autoptr (GmCutout) cutout = g_list_model_get_item (cutouts, i); + const GmRect *phys_bounds = gm_cutout_get_bounds (cutout); + GdkRectangle bounds; + int available_space; + + bounds = (GdkRectangle) { + .x = floor (phys_bounds->x / scale), + .y = floor (phys_bounds->y / scale), + .width = ceil (phys_bounds->width / scale), + .height = ceil (phys_bounds->height / scale), + }; + + available_space = ((width - center_clock_rect.width) / 2) - (bounds.x + bounds.width) - + CORNER_CUTOUT_SHIFT_THRESHOLD; + if (gdk_rectangle_intersect (&bounds, &network_box, NULL) && available_space > 0) + *network = bounds.x + bounds.width + PHOSH_TOP_BAR_CUTOUT_MIN_PADDING; + + available_space = ((width - center_clock_rect.width) / 2) - (width - bounds.x) - + CORNER_CUTOUT_SHIFT_THRESHOLD; + if (gdk_rectangle_intersect (&bounds, &indicators_box, NULL) && available_space > 0) + *indicators = (self->builtin->width / scale - bounds.x) + PHOSH_TOP_BAR_CUTOUT_MIN_PADDING; + } + + out: + *network = MAX (*network, PHOSH_TOP_BAR_MIN_PADDING); + *indicators = MAX (*indicators, PHOSH_TOP_BAR_MIN_PADDING); + + g_debug ("Network box: %d, indicators shift: %d", *network, *indicators); +} + +/** + * get_corner_shift: + * @self: The layout manager + * + * Get the left and right margins to compensate for rounded corners. + * + * We assume the panel uses it's native orientation. + * + * Returns: The margin in pixels + */ +static guint +get_corner_shift (PhoshLayoutManager *self) +{ + float r, a, b, c, scale; + PhoshShellLayout layout; + guint shift = PHOSH_TOP_BAR_MIN_PADDING; + + layout = g_settings_get_enum (self->settings, SHELL_LAYOUT_KEY); + if (layout != PHOSH_SHELL_LAYOUT_DEVICE) + goto out; + + scale = phosh_monitor_get_fractional_scale (self->builtin); +G_GNUC_BEGIN_IGNORE_DEPRECATIONS + r = c = gm_display_panel_get_border_radius (self->panel) / scale; +G_GNUC_END_IGNORE_DEPRECATIONS + + if (G_APPROX_VALUE (r, 0, FLT_EPSILON)) + goto out; + + /* + * We want to keep the n pixels towards the top screen edge clear + * n + b = r and m + a = r and c = r + * + * ------------------------ + * |----+------------------ n + * | |\ + * | b | \ c = r + * | | \ + * +----+---* + * | m a + */ + /* Icons usually don't fill the full height so assume 80% */ + b = c - (PHOSH_TOP_BAR_HEIGHT - 0.8 * PHOSH_TOP_BAR_ICON_PIXEL_SIZE) / 2; + a = floor (sqrt((c * c) - (b * b))); + + shift = MAX (PHOSH_TOP_BAR_MIN_PADDING, ceil (r - a)); + + out: + g_debug ("Corner shift: %u", shift); + + return shift; +} + + +static void +update_layout (PhoshLayoutManager *self) +{ + PhoshLayoutClockPosition pos; + guint corner_shift, clock_shift = 0; + guint network_box_shift = 0, indicators_box_shift = 0; + + corner_shift = get_corner_shift (self); + get_cutout_shifts (self, &network_box_shift, &indicators_box_shift); + pos = get_clock_pos (self, &clock_shift); + + indicators_box_shift = MAX (indicators_box_shift, corner_shift); + network_box_shift = MAX (network_box_shift, corner_shift); + + if (self->corner_shift == corner_shift && + self->network_box_shift == network_box_shift && + self->indicators_box_shift == indicators_box_shift && + self->clock_pos == pos && + self->clock_shift == clock_shift) { + return; + } + + self->clock_pos = pos; + self->clock_shift = clock_shift; + self->corner_shift = corner_shift; + self->network_box_shift = network_box_shift; + self->indicators_box_shift = indicators_box_shift; + + g_signal_emit (self, signals[LAYOUT_CHANGED], 0); +} + + +static void +on_builtin_monitor_configured (PhoshLayoutManager *self, PhoshMonitor *monitor) +{ + g_return_if_fail (PHOSH_IS_LAYOUT_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + update_layout (self); +} + + +static void +on_builtin_monitor_changed (PhoshLayoutManager *self, + GParamSpec *pspec, + PhoshShell *shell) +{ + PhoshMonitor *monitor; + + g_return_if_fail (PHOSH_IS_LAYOUT_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SHELL (shell)); + g_return_if_fail (GM_IS_DISPLAY_PANEL (self->panel)); + + monitor = phosh_shell_get_builtin_monitor (shell); + if (self->builtin == monitor) + return; + + self->builtin = monitor; + if (monitor == NULL) + return; + + g_signal_connect_object (monitor, + "configured", + G_CALLBACK (on_builtin_monitor_configured), + self, + G_CONNECT_SWAPPED); + if (phosh_monitor_is_configured (monitor)) + on_builtin_monitor_configured (self, monitor); +} + + +static void +phosh_layout_manager_finalize (GObject *object) +{ + PhoshLayoutManager *self = PHOSH_LAYOUT_MANAGER(object); + + self->builtin = NULL; + g_clear_object (&self->settings); + g_clear_object (&self->panel); + + G_OBJECT_CLASS (phosh_layout_manager_parent_class)->finalize (object); +} + + +static void +phosh_layout_manager_class_init (PhoshLayoutManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_layout_manager_finalize; + + signals[LAYOUT_CHANGED] = g_signal_new ("layout-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_layout_manager_init (PhoshLayoutManager *self) +{ + g_autoptr (GError) err = NULL; + g_auto (GStrv) compatibles = NULL; + PhoshShell *shell = phosh_shell_get_default (); + g_autoptr (GmDeviceInfo) info = NULL; + + self->network_box_shift = PHOSH_TOP_BAR_MIN_PADDING; + self->indicators_box_shift = PHOSH_TOP_BAR_MIN_PADDING; + self->corner_shift = PHOSH_TOP_BAR_MIN_PADDING; + + self->settings = g_settings_new ("sm.puri.phosh"); + + compatibles = gm_device_tree_get_compatibles (NULL, &err); + if (compatibles != NULL) { + info = gm_device_info_new ((const char * const *)compatibles); + g_set_object (&self->panel, gm_device_info_get_display_panel (info)); + } + + if (self->panel) { + g_signal_connect_swapped (shell, "notify::builtin-monitor", + G_CALLBACK (on_builtin_monitor_changed), + self); + on_builtin_monitor_changed (self, NULL, shell); + } +} + + +PhoshLayoutManager * +phosh_layout_manager_new (void) +{ + return PHOSH_LAYOUT_MANAGER (g_object_new (PHOSH_TYPE_LAYOUT_MANAGER, NULL)); +} + +/** + * phosh_layout_manager_get_clock_pos: + * @self: The layout manager + * + * The top bar's clock position. + * + * Returns: The position + */ +PhoshLayoutClockPosition +phosh_layout_manager_get_clock_pos (PhoshLayoutManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LAYOUT_MANAGER (self), PHOSH_LAYOUT_CLOCK_POS_CENTER); + + return self->clock_pos; +} + +/** + * phosh_layout_manager_get_clock_shift: + * @self: The layout manager + * + * The settings clock position + * + * Returns: How far the settings clock is shifted down + */ +guint +phosh_layout_manager_get_clock_shift (PhoshLayoutManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LAYOUT_MANAGER (self), 0); + + return self->clock_shift; +} + +/** + * phosh_layout_manager_get_box_shifts: + * @self: The layout manager + * @network_shift: (out): The shift of the network box + * @indicators_shift: (out): The shift of the indicators box + * @settings_shift (out): The shift of the settings launcher at the bottom of the top-panel + * when unfolded + * + * Gets the amount of pixels UI elements should be moved to towards the + * center because of rounded corners or notches. + */ +void +phosh_layout_manager_get_box_shifts (PhoshLayoutManager *self, + guint *network_shift, + guint *indicators_shift, + guint *settings_shift) +{ + g_return_if_fail (PHOSH_IS_LAYOUT_MANAGER (self)); + g_return_if_fail (network_shift && indicators_shift); + + *network_shift = self->network_box_shift; + *indicators_shift = self->indicators_box_shift; + *settings_shift = self->corner_shift; +} diff --git a/src/layout-manager.h b/src/layout-manager.h new file mode 100644 index 000000000..5dd49a21c --- /dev/null +++ b/src/layout-manager.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2023-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "monitor/monitor.h" + +#include +#include "phosh-settings-enums.h" + +#pragma once + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_LAYOUT_MANAGER (phosh_layout_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLayoutManager, phosh_layout_manager, PHOSH, LAYOUT_MANAGER, GObject) + +PhoshLayoutManager *phosh_layout_manager_new (void); +PhoshLayoutClockPosition phosh_layout_manager_get_clock_pos (PhoshLayoutManager *self); +guint phosh_layout_manager_get_clock_shift (PhoshLayoutManager *self); +void phosh_layout_manager_get_box_shifts (PhoshLayoutManager *self, + guint *network_shift, + guint *indicators_shift, + guint *settings_shift); +G_END_DECLS diff --git a/src/libphosh-abi.dump b/src/libphosh-abi.dump new file mode 100644 index 000000000..a4715f211 --- /dev/null +++ b/src/libphosh-abi.dump @@ -0,0 +1,6157 @@ +$VAR1 = { + 'ABI_DUMPER_VERSION' => '1.4', + 'ABI_DUMP_VERSION' => '3.5', + 'Arch' => 'x86_64', + 'GccVersion' => '15.2.0', + 'Headers' => { + 'layersurface.h' => 1, + 'lockscreen-manager.h' => 1, + 'lockscreen.h' => 1, + 'quick-setting.h' => 1, + 'screenshot-manager.h' => 1, + 'shell.h' => 1, + 'status-icon.h' => 1, + 'status-page.h' => 1, + 'wall-clock.h' => 1 + }, + 'Language' => 'C', + 'LibraryName' => 'libphosh-0.45.so.0', + 'LibraryVersion' => '0.51.rc1', + 'NameSpaces' => {}, + 'Needed' => { + 'libappstream.so.5' => 1, + 'libc.so.6' => 1, + 'libcairo.so.2' => 1, + 'libcallaudio-0.1.so.0' => 1, + 'libfeedback-0.0.so.0' => 1, + 'libfribidi.so.0' => 1, + 'libgcr-base-3.so.1' => 1, + 'libgcr-ui-3.so.1' => 1, + 'libgdk-3.so.0' => 1, + 'libgdk_pixbuf-2.0.so.0' => 1, + 'libgio-2.0.so.0' => 1, + 'libglib-2.0.so.0' => 1, + 'libgmobile.so.0' => 1, + 'libgmodule-2.0.so.0' => 1, + 'libgnome-bluetooth-3.0.so.13' => 1, + 'libgnome-desktop-3.so.20' => 1, + 'libgobject-2.0.so.0' => 1, + 'libgtk-3.so.0' => 1, + 'libgudev-1.0.so.0' => 1, + 'libhandy-1.so.0' => 1, + 'libm.so.6' => 1, + 'libmm-glib.so.0' => 1, + 'libnm.so.0' => 1, + 'libpam.so.0' => 1, + 'libpango-1.0.so.0' => 1, + 'libpolkit-agent-1.so.0' => 1, + 'libpolkit-gobject-1.so.0' => 1, + 'libpulse-mainloop-glib.so.0' => 1, + 'libpulse.so.0' => 1, + 'libsecret-1.so.0' => 1, + 'libsoup-3.0.so.0' => 1, + 'libsystemd.so.0' => 1, + 'libupower-glib.so.3' => 1, + 'libwayland-client.so.0' => 1 + }, + 'PublicABI' => '1', + 'Sources' => { + 'animation.c' => 1, + 'background-image.c' => 1, + 'calls-manager.c' => 1, + 'fader.c' => 1, + 'layersurface.c' => 1, + 'lockscreen-manager.c' => 1, + 'lockscreen.c' => 1, + 'phosh-screenshot-dbus.c' => 1, + 'quick-setting.c' => 1, + 'screenshot-manager.c' => 1, + 'shell.c' => 1, + 'status-icon.c' => 1, + 'status-page.c' => 1, + 'wall-clock.c' => 1 + }, + 'SymbolInfo' => { + '1072441' => { + 'Header' => 'status-page.h', + 'Line' => '14', + 'Return' => '1866', + 'ShortName' => 'phosh_status_page_get_type', + 'Source' => 'status-page.c', + 'SourceLine' => '58' + }, + '1128788' => { + 'Header' => 'layersurface.h', + 'Line' => '15', + 'Return' => '1866', + 'ShortName' => 'phosh_layer_surface_get_type', + 'Source' => 'layersurface.c', + 'SourceLine' => '80' + }, + '1136150' => { + 'Header' => 'wall-clock.h', + 'Line' => '43', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1135071' + }, + '1' => { + 'name' => 'datetime', + 'type' => '1133268' + }, + '2' => { + 'name' => 'clock_format', + 'type' => '1132751' + }, + '3' => { + 'name' => 'show_full_date', + 'type' => '253' + } + }, + 'Return' => '190', + 'ShortName' => 'phosh_wall_clock_string_for_datetime', + 'Source' => 'wall-clock.c', + 'SourceLine' => '298' + }, + '1136392' => { + 'Header' => 'wall-clock.h', + 'Line' => '17', + 'Return' => '1866', + 'ShortName' => 'phosh_wall_clock_get_type', + 'Source' => 'wall-clock.c', + 'SourceLine' => '36' + }, + '1288601' => { + 'Header' => 'lockscreen.h', + 'Line' => '58', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + }, + '1' => { + 'name' => 'page', + 'type' => '1287258' + } + }, + 'Reg' => { + '1' => 'rbp' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_set_page', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1175' + }, + '1288624' => { + 'Header' => 'lockscreen.h', + 'Line' => '60', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + } + }, + 'Return' => '1287258', + 'ShortName' => 'phosh_lockscreen_get_page', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1143' + }, + '1288788' => { + 'Header' => 'lockscreen.h', + 'Line' => '31', + 'Return' => '1866', + 'ShortName' => 'phosh_lockscreen_get_type', + 'Source' => 'lockscreen.c', + 'SourceLine' => '124' + }, + '1288883' => { + 'Header' => 'shell.h', + 'Line' => '43', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + } + }, + 'Return' => '1866', + 'ShortName' => 'phosh_shell_get_lockscreen_type', + 'Source' => 'shell.c', + 'SourceLine' => '2679' + }, + '1290560' => { + 'Header' => 'lockscreen-manager.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '1287309', + 'ShortName' => 'phosh_lockscreen_manager_get_lockscreen', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '647' + }, + '1290860' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '25', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + }, + '1' => { + 'name' => 'page', + 'type' => '1287258' + } + }, + 'Reg' => { + '0' => 'rbp', + '1' => 'r14' + }, + 'Return' => '253', + 'ShortName' => 'phosh_lockscreen_manager_set_page', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '623' + }, + '1291205' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '28', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '140', + 'ShortName' => 'phosh_lockscreen_manager_get_active_time', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '614' + }, + '1291404' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '27', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '1287258', + 'ShortName' => 'phosh_lockscreen_manager_get_page', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '605' + }, + '1291595' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '24', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + } + }, + 'Reg' => { + '0' => 'rbp' + }, + 'Return' => '253', + 'ShortName' => 'phosh_lockscreen_manager_get_locked', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '591' + }, + '1291794' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '22', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287544' + }, + '1' => { + 'name' => 'lock', + 'type' => '253' + } + }, + 'Reg' => { + '0' => 'rbx', + '1' => 'rbp' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_manager_set_locked', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '577' + }, + '1303255' => { + 'Header' => 'lockscreen-manager.h', + 'Line' => '14', + 'Return' => '1866', + 'ShortName' => 'phosh_lockscreen_manager_get_type', + 'Source' => 'lockscreen-manager.c', + 'SourceLine' => '76' + }, + '1323514' => { + 'Header' => 'wall-clock.h', + 'Line' => '42', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1135071' + } + }, + 'Return' => '190', + 'ShortName' => 'phosh_wall_clock_local_date', + 'Source' => 'wall-clock.c', + 'SourceLine' => '265' + }, + '1323586' => { + 'Header' => 'wall-clock.h', + 'Line' => '41', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1135071' + }, + '1' => { + 'name' => 'time_only', + 'type' => '253' + } + }, + 'Return' => '207', + 'ShortName' => 'phosh_wall_clock_get_clock', + 'Source' => 'wall-clock.c', + 'SourceLine' => '217' + }, + '1324293' => { + 'Header' => 'wall-clock.h', + 'Line' => '40', + 'Return' => '1135071', + 'ShortName' => 'phosh_wall_clock_get_default', + 'Source' => 'wall-clock.c', + 'SourceLine' => '198' + }, + '1326556' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + }, + '1' => { + 'name' => 'status', + 'type' => '207' + } + }, + 'Reg' => { + '0' => 'rbx', + '1' => 'r14' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_set_unlock_status', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1309' + }, + '1326864' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + }, + '1' => { + 'name' => 'widget', + 'type' => '943522' + } + }, + 'Reg' => { + '0' => 'rbx', + '1' => 'rbp' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_add_extra_page', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1291' + }, + '1327177' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_shake_pin_entry', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1264' + }, + '1327570' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_clear_pin_entry', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1247' + }, + '1327842' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '207', + 'ShortName' => 'phosh_lockscreen_get_pin_entry', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1231' + }, + '1328123' => { + 'Header' => 'lockscreen.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1287309' + }, + '1' => { + 'name' => 'page', + 'type' => '1287258' + } + }, + 'Reg' => { + '0' => 'rbx', + '1' => 'r14' + }, + 'Return' => '1', + 'ShortName' => 'phosh_lockscreen_set_default_page', + 'Source' => 'lockscreen.c', + 'SourceLine' => '1213' + }, + '1362706' => { + 'Header' => 'shell.h', + 'Line' => '44', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + } + }, + 'Reg' => { + '0' => 'rbp' + }, + 'Return' => '253', + 'ShortName' => 'phosh_shell_get_locked', + 'Source' => 'shell.c', + 'SourceLine' => '2549' + }, + '1637841' => { + 'Header' => 'screenshot-manager.h', + 'Line' => '21', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1633096' + }, + '1' => { + 'name' => 'area', + 'type' => '1221462' + }, + '2' => { + 'name' => 'filename', + 'type' => '207' + }, + '3' => { + 'name' => 'copy_to_clipboard', + 'type' => '253' + }, + '4' => { + 'name' => 'include_cursor', + 'type' => '253' + } + }, + 'Reg' => { + '3' => 'rdx' + }, + 'Return' => '253', + 'ShortName' => 'phosh_screenshot_manager_take_screenshot', + 'Source' => 'screenshot-manager.c', + 'SourceLine' => '1370' + }, + '1638034' => { + 'Header' => 'screenshot-manager.h', + 'Line' => '20', + 'Return' => '1633096', + 'ShortName' => 'phosh_screenshot_manager_new', + 'Source' => 'screenshot-manager.c', + 'SourceLine' => '1351' + }, + '1666371' => { + 'Header' => 'screenshot-manager.h', + 'Line' => '17', + 'Return' => '1866', + 'ShortName' => 'phosh_screenshot_manager_get_type', + 'Source' => 'screenshot-manager.c', + 'SourceLine' => '107' + }, + '1678631' => { + 'Header' => 'shell.h', + 'Line' => '51', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + }, + '1' => { + 'name' => 'timeout', + 'type' => '277' + } + }, + 'Reg' => { + '0' => 'r14', + '1' => 'r13' + }, + 'Return' => '1', + 'ShortName' => 'phosh_shell_fade_out', + 'Source' => 'shell.c', + 'SourceLine' => '2308' + }, + '1706595' => { + 'Header' => 'quick-setting.h', + 'Line' => '16', + 'Return' => '1866', + 'ShortName' => 'phosh_quick_setting_get_type', + 'Source' => 'quick-setting.c', + 'SourceLine' => '100' + }, + '1717748' => { + 'Header' => 'shell.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '1', + 'ShortName' => 'phosh_shell_set_default', + 'Source' => 'shell.c', + 'SourceLine' => '2282' + }, + '1718219' => { + 'Header' => 'shell.h', + 'Line' => '45', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + }, + '1' => { + 'name' => 'x', + 'type' => '1355691' + }, + '2' => { + 'name' => 'y', + 'type' => '1355691' + }, + '3' => { + 'name' => 'width', + 'type' => '1355691' + }, + '4' => { + 'name' => 'height', + 'type' => '1355691' + } + }, + 'Reg' => { + '1' => 'r14', + '2' => 'r15', + '3' => 'r12', + '4' => 'r13' + }, + 'Return' => '1', + 'ShortName' => 'phosh_shell_get_usable_area', + 'Source' => 'shell.c', + 'SourceLine' => '2204' + }, + '1723914' => { + 'Header' => 'shell.h', + 'Line' => '55', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + } + }, + 'Return' => '1633096', + 'ShortName' => 'phosh_shell_get_screenshot_manager', + 'Source' => 'shell.c', + 'SourceLine' => '1860' + }, + '1726074' => { + 'Header' => 'shell.h', + 'Line' => '54', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '915140' + } + }, + 'Return' => '1287544', + 'ShortName' => 'phosh_shell_get_lockscreen_manager', + 'Source' => 'shell.c', + 'SourceLine' => '1740' + }, + '1731979' => { + 'Header' => 'shell.h', + 'Return' => '915140', + 'ShortName' => 'phosh_shell_new', + 'Source' => 'shell.c', + 'SourceLine' => '1411' + }, + '2993676' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '207', + 'ShortName' => 'phosh_quick_setting_get_long_press_action_target', + 'Source' => 'quick-setting.c', + 'SourceLine' => '700' + }, + '2993777' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'action_target', + 'type' => '207' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_long_press_action_target', + 'Source' => 'quick-setting.c', + 'SourceLine' => '686' + }, + '2994274' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '207', + 'ShortName' => 'phosh_quick_setting_get_long_press_action_name', + 'Source' => 'quick-setting.c', + 'SourceLine' => '673' + }, + '2994370' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'action_name', + 'type' => '207' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_long_press_action_name', + 'Source' => 'quick-setting.c', + 'SourceLine' => '659' + }, + '2994867' => { + 'Header' => 'quick-setting.h', + 'Line' => '44', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '2991339', + 'ShortName' => 'phosh_quick_setting_get_status_page', + 'Source' => 'quick-setting.c', + 'SourceLine' => '646' + }, + '2994963' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'status_page', + 'type' => '2991339' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_status_page', + 'Source' => 'quick-setting.c', + 'SourceLine' => '598' + }, + '2996817' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '253', + 'ShortName' => 'phosh_quick_setting_get_can_show_status', + 'Source' => 'quick-setting.c', + 'SourceLine' => '509' + }, + '2996913' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'can_show_status', + 'type' => '253' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_can_show_status', + 'Source' => 'quick-setting.c', + 'SourceLine' => '489' + }, + '2997302' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '253', + 'ShortName' => 'phosh_quick_setting_get_showing_status', + 'Source' => 'quick-setting.c', + 'SourceLine' => '476' + }, + '2997398' => { + 'Header' => 'quick-setting.h', + 'Line' => '39', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'showing_status', + 'type' => '253' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_showing_status', + 'Source' => 'quick-setting.c', + 'SourceLine' => '450' + }, + '2997497' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + } + }, + 'Return' => '253', + 'ShortName' => 'phosh_quick_setting_get_active', + 'Source' => 'quick-setting.c', + 'SourceLine' => '437' + }, + '2997593' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991562' + }, + '1' => { + 'name' => 'active', + 'type' => '253' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_quick_setting_set_active', + 'Source' => 'quick-setting.c', + 'SourceLine' => '414' + }, + '2998029' => { + 'Header' => 'quick-setting.h', + 'Param' => { + '0' => { + 'name' => 'status_page', + 'type' => '2991339' + } + }, + 'Return' => '943522', + 'ShortName' => 'phosh_quick_setting_new', + 'Source' => 'quick-setting.c', + 'SourceLine' => '407' + }, + '3102107' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + } + }, + 'Return' => '190', + 'ShortName' => 'phosh_status_icon_get_info', + 'Source' => 'status-icon.c', + 'SourceLine' => '463' + }, + '3102716' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + } + }, + 'Return' => '277', + 'ShortName' => 'phosh_status_icon_get_pixel_size', + 'Source' => 'status-icon.c', + 'SourceLine' => '407' + }, + '3102812' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + }, + '1' => { + 'name' => 'size', + 'type' => '277' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_icon_set_pixel_size', + 'Source' => 'status-icon.c', + 'SourceLine' => '389' + }, + '3102895' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + } + }, + 'Return' => '2040796', + 'ShortName' => 'phosh_status_icon_get_icon_size', + 'Source' => 'status-icon.c', + 'SourceLine' => '376' + }, + '3102991' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + }, + '1' => { + 'name' => 'size', + 'type' => '2040796' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_icon_set_icon_size', + 'Source' => 'status-icon.c', + 'SourceLine' => '320' + }, + '3103512' => { + 'Header' => 'status-icon.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + } + }, + 'Reg' => { + '0' => 'rbx' + }, + 'Return' => '190', + 'ShortName' => 'phosh_status_icon_get_icon_name', + 'Source' => 'status-icon.c', + 'SourceLine' => '296' + }, + '3104196' => { + 'Header' => 'status-icon.h', + 'Return' => '943522', + 'ShortName' => 'phosh_status_icon_new', + 'Source' => 'status-icon.c', + 'SourceLine' => '269' + }, + '3146928' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + } + }, + 'Return' => '943522', + 'ShortName' => 'phosh_status_page_get_footer', + 'Source' => 'status-page.c', + 'SourceLine' => '394' + }, + '3147027' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + }, + '1' => { + 'name' => 'footer_widget', + 'type' => '943522' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_page_set_footer', + 'Source' => 'status-page.c', + 'SourceLine' => '359' + }, + '3147613' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + } + }, + 'Return' => '943522', + 'ShortName' => 'phosh_status_page_get_content', + 'Source' => 'status-page.c', + 'SourceLine' => '341' + }, + '3147707' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + }, + '1' => { + 'name' => 'content_widget', + 'type' => '943522' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_page_set_content', + 'Source' => 'status-page.c', + 'SourceLine' => '309' + }, + '3148245' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + } + }, + 'Return' => '943522', + 'ShortName' => 'phosh_status_page_get_header', + 'Source' => 'status-page.c', + 'SourceLine' => '290' + }, + '3148318' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + }, + '1' => { + 'name' => 'header_widget', + 'type' => '943522' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_page_set_header', + 'Source' => 'status-page.c', + 'SourceLine' => '256' + }, + '3148885' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + } + }, + 'Return' => '207', + 'ShortName' => 'phosh_status_page_get_title', + 'Source' => 'status-page.c', + 'SourceLine' => '238' + }, + '3148980' => { + 'Header' => 'status-page.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '2991339' + }, + '1' => { + 'name' => 'title', + 'type' => '207' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_page_set_title', + 'Source' => 'status-page.c', + 'SourceLine' => '219' + }, + '3149073' => { + 'Header' => 'status-page.h', + 'Return' => '2991339', + 'ShortName' => 'phosh_status_page_new', + 'Source' => 'status-page.c', + 'SourceLine' => '212' + }, + '3302228' => { + 'Header' => 'wall-clock.h', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '1135071' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_wall_clock_set_default', + 'Source' => 'wall-clock.c', + 'SourceLine' => '180' + }, + '3302619' => { + 'Header' => 'wall-clock.h', + 'Return' => '1135071', + 'ShortName' => 'phosh_wall_clock_new', + 'Source' => 'wall-clock.c', + 'SourceLine' => '165' + }, + '916037' => { + 'Header' => 'shell.h', + 'Line' => '41', + 'Return' => '915140', + 'ShortName' => 'phosh_shell_get_default', + 'Source' => 'shell.c', + 'SourceLine' => '2300' + }, + '963956' => { + 'Header' => 'shell.h', + 'Line' => '20', + 'Return' => '1866', + 'ShortName' => 'phosh_shell_get_type', + 'Source' => 'shell.c', + 'SourceLine' => '226' + }, + '992566' => { + 'Header' => 'status-icon.h', + 'Line' => '47', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + } + }, + 'Return' => '943522', + 'ShortName' => 'phosh_status_icon_get_extra_widget', + 'Source' => 'status-icon.c', + 'SourceLine' => '450' + }, + '993009' => { + 'Header' => 'status-icon.h', + 'Line' => '44', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + }, + '1' => { + 'name' => 'icon_name', + 'type' => '207' + } + }, + 'Reg' => { + '0' => 'rbx', + '1' => 'r12' + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_icon_set_icon_name', + 'Source' => 'status-icon.c', + 'SourceLine' => '276' + }, + '993032' => { + 'Header' => 'status-icon.h', + 'Line' => '48', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + }, + '1' => { + 'name' => 'info', + 'type' => '207' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_icon_set_info', + 'Source' => 'status-icon.c', + 'SourceLine' => '476' + }, + '993097' => { + 'Header' => 'status-icon.h', + 'Line' => '46', + 'Param' => { + '0' => { + 'name' => 'self', + 'type' => '991743' + }, + '1' => { + 'name' => 'widget', + 'type' => '943522' + } + }, + 'Return' => '1', + 'ShortName' => 'phosh_status_icon_set_extra_widget', + 'Source' => 'status-icon.c', + 'SourceLine' => '420' + }, + '993195' => { + 'Header' => 'status-icon.h', + 'Line' => '15', + 'Return' => '1866', + 'ShortName' => 'phosh_status_icon_get_type', + 'Source' => 'status-icon.c', + 'SourceLine' => '49' + } + }, + 'SymbolVersion' => { + 'gtk_filter_list_model_get_model' => 'gtk_filter_list_model_get_model@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_get_type' => 'gtk_filter_list_model_get_type@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_has_filter' => 'gtk_filter_list_model_has_filter@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_new' => 'gtk_filter_list_model_new@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_new_for_type' => 'gtk_filter_list_model_new_for_type@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_refilter' => 'gtk_filter_list_model_refilter@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_set_filter_func' => 'gtk_filter_list_model_set_filter_func@@LIBPHOSH_0_45_0', + 'gtk_filter_list_model_set_model' => 'gtk_filter_list_model_set_model@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_get_model' => 'gtk_sort_list_model_get_model@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_get_type' => 'gtk_sort_list_model_get_type@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_has_sort' => 'gtk_sort_list_model_has_sort@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_new' => 'gtk_sort_list_model_new@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_new_for_type' => 'gtk_sort_list_model_new_for_type@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_resort' => 'gtk_sort_list_model_resort@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_set_model' => 'gtk_sort_list_model_set_model@@LIBPHOSH_0_45_0', + 'gtk_sort_list_model_set_sort_func' => 'gtk_sort_list_model_set_sort_func@@LIBPHOSH_0_45_0', + 'phosh_dbus_screenshot_get_type' => 'phosh_dbus_screenshot_get_type@@LIBPHOSH_0_45_0', + 'phosh_dbus_screenshot_proxy_get_type' => 'phosh_dbus_screenshot_proxy_get_type@@LIBPHOSH_0_45_0', + 'phosh_dbus_screenshot_skeleton_get_type' => 'phosh_dbus_screenshot_skeleton_get_type@@LIBPHOSH_0_45_0', + 'phosh_layer_surface_get_type' => 'phosh_layer_surface_get_type@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_add_extra_page' => 'phosh_lockscreen_add_extra_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_clear_pin_entry' => 'phosh_lockscreen_clear_pin_entry@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_get_page' => 'phosh_lockscreen_get_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_get_pin_entry' => 'phosh_lockscreen_get_pin_entry@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_get_type' => 'phosh_lockscreen_get_type@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_get_active_time' => 'phosh_lockscreen_manager_get_active_time@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_get_locked' => 'phosh_lockscreen_manager_get_locked@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_get_lockscreen' => 'phosh_lockscreen_manager_get_lockscreen@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_get_page' => 'phosh_lockscreen_manager_get_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_get_type' => 'phosh_lockscreen_manager_get_type@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_set_locked' => 'phosh_lockscreen_manager_set_locked@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_manager_set_page' => 'phosh_lockscreen_manager_set_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_page_get_type' => 'phosh_lockscreen_page_get_type@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_set_default_page' => 'phosh_lockscreen_set_default_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_set_page' => 'phosh_lockscreen_set_page@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_set_unlock_status' => 'phosh_lockscreen_set_unlock_status@@LIBPHOSH_0_45_0', + 'phosh_lockscreen_shake_pin_entry' => 'phosh_lockscreen_shake_pin_entry@@LIBPHOSH_0_45_0', + 'phosh_media_player_get_type' => 'phosh_media_player_get_type@@LIBPHOSH_0_45_0', + 'phosh_media_player_new' => 'phosh_media_player_new@@LIBPHOSH_0_45_0', + 'phosh_media_player_set_player' => 'phosh_media_player_set_player@@LIBPHOSH_0_45_0', + 'phosh_monitor_get_fractional_scale' => 'phosh_monitor_get_fractional_scale@@LIBPHOSH_0_45_0', + 'phosh_monitor_get_type' => 'phosh_monitor_get_type@@LIBPHOSH_0_45_0', + 'phosh_monitor_manager_apply_monitor_config' => 'phosh_monitor_manager_apply_monitor_config@@LIBPHOSH_0_45_0', + 'phosh_monitor_manager_get_night_light_supported' => 'phosh_monitor_manager_get_night_light_supported@@LIBPHOSH_0_45_0', + 'phosh_monitor_manager_get_type' => 'phosh_monitor_manager_get_type@@LIBPHOSH_0_45_0', + 'phosh_monitor_manager_set_monitor_scale' => 'phosh_monitor_manager_set_monitor_scale@@LIBPHOSH_0_45_0', + 'phosh_mpris_manager_get_known_players' => 'phosh_mpris_manager_get_known_players@@LIBPHOSH_0_45_0', + 'phosh_mpris_manager_get_type' => 'phosh_mpris_manager_get_type@@LIBPHOSH_0_45_0', + 'phosh_notification_get_type' => 'phosh_notification_get_type@@LIBPHOSH_0_45_0', + 'phosh_notify_manager_add_shell_notification' => 'phosh_notify_manager_add_shell_notification@@LIBPHOSH_0_45_0', + 'phosh_notify_manager_get_default' => 'phosh_notify_manager_get_default@@LIBPHOSH_0_45_0', + 'phosh_notify_manager_get_type' => 'phosh_notify_manager_get_type@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_active' => 'phosh_quick_setting_get_active@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_can_show_status' => 'phosh_quick_setting_get_can_show_status@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_long_press_action_name' => 'phosh_quick_setting_get_long_press_action_name@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_long_press_action_target' => 'phosh_quick_setting_get_long_press_action_target@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_showing_status' => 'phosh_quick_setting_get_showing_status@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_status_page' => 'phosh_quick_setting_get_status_page@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_get_type' => 'phosh_quick_setting_get_type@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_new' => 'phosh_quick_setting_new@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_active' => 'phosh_quick_setting_set_active@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_can_show_status' => 'phosh_quick_setting_set_can_show_status@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_long_press_action_name' => 'phosh_quick_setting_set_long_press_action_name@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_long_press_action_target' => 'phosh_quick_setting_set_long_press_action_target@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_showing_status' => 'phosh_quick_setting_set_showing_status@@LIBPHOSH_0_45_0', + 'phosh_quick_setting_set_status_page' => 'phosh_quick_setting_set_status_page@@LIBPHOSH_0_45_0', + 'phosh_screenshot_manager_get_type' => 'phosh_screenshot_manager_get_type@@LIBPHOSH_0_45_0', + 'phosh_screenshot_manager_new' => 'phosh_screenshot_manager_new@@LIBPHOSH_0_45_0', + 'phosh_screenshot_manager_take_screenshot' => 'phosh_screenshot_manager_take_screenshot@@LIBPHOSH_0_45_0', + 'phosh_session_manager_inhibit' => 'phosh_session_manager_inhibit@@LIBPHOSH_0_45_0', + 'phosh_session_manager_uninhibit' => 'phosh_session_manager_uninhibit@@LIBPHOSH_0_45_0', + 'phosh_shell_fade_out' => 'phosh_shell_fade_out@@LIBPHOSH_0_45_0', + 'phosh_shell_get_default' => 'phosh_shell_get_default@@LIBPHOSH_0_45_0', + 'phosh_shell_get_launcher_entry_manager' => 'phosh_shell_get_launcher_entry_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_locked' => 'phosh_shell_get_locked@@LIBPHOSH_0_45_0', + 'phosh_shell_get_lockscreen_manager' => 'phosh_shell_get_lockscreen_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_lockscreen_type' => 'phosh_shell_get_lockscreen_type@@LIBPHOSH_0_45_0', + 'phosh_shell_get_monitor_manager' => 'phosh_shell_get_monitor_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_mpris_manager' => 'phosh_shell_get_mpris_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_primary_monitor' => 'phosh_shell_get_primary_monitor@@LIBPHOSH_0_45_0', + 'phosh_shell_get_screenshot_manager' => 'phosh_shell_get_screenshot_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_session_manager' => 'phosh_shell_get_session_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_type' => 'phosh_shell_get_type@@LIBPHOSH_0_45_0', + 'phosh_shell_get_usable_area' => 'phosh_shell_get_usable_area@@LIBPHOSH_0_45_0', + 'phosh_shell_get_wifi_manager' => 'phosh_shell_get_wifi_manager@@LIBPHOSH_0_45_0', + 'phosh_shell_get_wwan' => 'phosh_shell_get_wwan@@LIBPHOSH_0_45_0', + 'phosh_shell_new' => 'phosh_shell_new@@LIBPHOSH_0_45_0', + 'phosh_shell_set_default' => 'phosh_shell_set_default@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_extra_widget' => 'phosh_status_icon_get_extra_widget@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_icon_name' => 'phosh_status_icon_get_icon_name@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_icon_size' => 'phosh_status_icon_get_icon_size@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_info' => 'phosh_status_icon_get_info@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_pixel_size' => 'phosh_status_icon_get_pixel_size@@LIBPHOSH_0_45_0', + 'phosh_status_icon_get_type' => 'phosh_status_icon_get_type@@LIBPHOSH_0_45_0', + 'phosh_status_icon_new' => 'phosh_status_icon_new@@LIBPHOSH_0_45_0', + 'phosh_status_icon_set_extra_widget' => 'phosh_status_icon_set_extra_widget@@LIBPHOSH_0_45_0', + 'phosh_status_icon_set_icon_name' => 'phosh_status_icon_set_icon_name@@LIBPHOSH_0_45_0', + 'phosh_status_icon_set_icon_size' => 'phosh_status_icon_set_icon_size@@LIBPHOSH_0_45_0', + 'phosh_status_icon_set_info' => 'phosh_status_icon_set_info@@LIBPHOSH_0_45_0', + 'phosh_status_icon_set_pixel_size' => 'phosh_status_icon_set_pixel_size@@LIBPHOSH_0_45_0', + 'phosh_status_page_get_content' => 'phosh_status_page_get_content@@LIBPHOSH_0_45_0', + 'phosh_status_page_get_footer' => 'phosh_status_page_get_footer@@LIBPHOSH_0_45_0', + 'phosh_status_page_get_header' => 'phosh_status_page_get_header@@LIBPHOSH_0_45_0', + 'phosh_status_page_get_title' => 'phosh_status_page_get_title@@LIBPHOSH_0_45_0', + 'phosh_status_page_get_type' => 'phosh_status_page_get_type@@LIBPHOSH_0_45_0', + 'phosh_status_page_new' => 'phosh_status_page_new@@LIBPHOSH_0_45_0', + 'phosh_status_page_placeholder_get_icon_name' => 'phosh_status_page_placeholder_get_icon_name@@LIBPHOSH_0_45_0', + 'phosh_status_page_placeholder_get_title' => 'phosh_status_page_placeholder_get_title@@LIBPHOSH_0_45_0', + 'phosh_status_page_placeholder_get_type' => 'phosh_status_page_placeholder_get_type@@LIBPHOSH_0_45_0', + 'phosh_status_page_placeholder_set_icon_name' => 'phosh_status_page_placeholder_set_icon_name@@LIBPHOSH_0_45_0', + 'phosh_status_page_placeholder_set_title' => 'phosh_status_page_placeholder_set_title@@LIBPHOSH_0_45_0', + 'phosh_status_page_set_content' => 'phosh_status_page_set_content@@LIBPHOSH_0_45_0', + 'phosh_status_page_set_footer' => 'phosh_status_page_set_footer@@LIBPHOSH_0_45_0', + 'phosh_status_page_set_header' => 'phosh_status_page_set_header@@LIBPHOSH_0_45_0', + 'phosh_status_page_set_title' => 'phosh_status_page_set_title@@LIBPHOSH_0_45_0', + 'phosh_util_append_to_strv' => 'phosh_util_append_to_strv@@LIBPHOSH_0_45_0', + 'phosh_util_calculate_supported_mode_scales' => 'phosh_util_calculate_supported_mode_scales@@LIBPHOSH_0_45_0', + 'phosh_util_data_uri_to_pixbuf' => 'phosh_util_data_uri_to_pixbuf@@LIBPHOSH_0_45_0', + 'phosh_util_escape_markup' => 'phosh_util_escape_markup@@LIBPHOSH_0_45_0', + 'phosh_util_file_equal' => 'phosh_util_file_equal@@LIBPHOSH_0_45_0', + 'phosh_util_gesture_is_touch' => 'phosh_util_gesture_is_touch@@LIBPHOSH_0_45_0', + 'phosh_util_get_icon_by_wifi_strength' => 'phosh_util_get_icon_by_wifi_strength@@LIBPHOSH_0_45_0', + 'phosh_util_have_gnome_software' => 'phosh_util_have_gnome_software@@LIBPHOSH_0_45_0', + 'phosh_util_matches_app_info' => 'phosh_util_matches_app_info@@LIBPHOSH_0_45_0', + 'phosh_util_open_mobile_settings_panel' => 'phosh_util_open_mobile_settings_panel@@LIBPHOSH_0_45_0', + 'phosh_util_open_settings_panel' => 'phosh_util_open_settings_panel@@LIBPHOSH_0_45_0', + 'phosh_util_remove_from_strv' => 'phosh_util_remove_from_strv@@LIBPHOSH_0_45_0', + 'phosh_util_toggle_style_class' => 'phosh_util_toggle_style_class@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_get_clock' => 'phosh_wall_clock_get_clock@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_get_default' => 'phosh_wall_clock_get_default@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_get_type' => 'phosh_wall_clock_get_type@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_local_date' => 'phosh_wall_clock_local_date@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_new' => 'phosh_wall_clock_new@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_set_default' => 'phosh_wall_clock_set_default@@LIBPHOSH_0_45_0', + 'phosh_wall_clock_string_for_datetime' => 'phosh_wall_clock_string_for_datetime@@LIBPHOSH_0_45_0', + 'phosh_wifi_manager_get_enabled' => 'phosh_wifi_manager_get_enabled@@LIBPHOSH_0_45_0', + 'phosh_wifi_manager_get_state' => 'phosh_wifi_manager_get_state@@LIBPHOSH_0_45_0', + 'phosh_wifi_manager_is_hotspot_master' => 'phosh_wifi_manager_is_hotspot_master@@LIBPHOSH_0_45_0', + 'phosh_wifi_manager_set_hotspot_master' => 'phosh_wifi_manager_set_hotspot_master@@LIBPHOSH_0_45_0', + 'phosh_wwan_has_data' => 'phosh_wwan_has_data@@LIBPHOSH_0_45_0', + 'phosh_wwan_is_enabled' => 'phosh_wwan_is_enabled@@LIBPHOSH_0_45_0', + 'phosh_wwan_is_unlocked' => 'phosh_wwan_is_unlocked@@LIBPHOSH_0_45_0', + 'phosh_wwan_set_data_enabled' => 'phosh_wwan_set_data_enabled@@LIBPHOSH_0_45_0' + }, + 'Symbols' => { + 'libphosh-0.45.so.0' => { + 'gtk_filter_list_model_get_model@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_get_type@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_has_filter@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_new@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_new_for_type@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_refilter@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_set_filter_func@@LIBPHOSH_0_45_0' => 1, + 'gtk_filter_list_model_set_model@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_get_model@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_get_type@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_has_sort@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_new@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_new_for_type@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_resort@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_set_model@@LIBPHOSH_0_45_0' => 1, + 'gtk_sort_list_model_set_sort_func@@LIBPHOSH_0_45_0' => 1, + 'phosh_dbus_screenshot_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_dbus_screenshot_proxy_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_dbus_screenshot_skeleton_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_layer_surface_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_add_extra_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_clear_pin_entry@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_get_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_get_pin_entry@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_get_active_time@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_get_locked@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_get_lockscreen@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_get_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_set_locked@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_manager_set_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_page_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_set_default_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_set_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_set_unlock_status@@LIBPHOSH_0_45_0' => 1, + 'phosh_lockscreen_shake_pin_entry@@LIBPHOSH_0_45_0' => 1, + 'phosh_media_player_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_media_player_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_media_player_set_player@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_get_fractional_scale@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_manager_apply_monitor_config@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_manager_get_night_light_supported@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_manager_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_monitor_manager_set_monitor_scale@@LIBPHOSH_0_45_0' => 1, + 'phosh_mpris_manager_get_known_players@@LIBPHOSH_0_45_0' => 1, + 'phosh_mpris_manager_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_notification_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_notify_manager_add_shell_notification@@LIBPHOSH_0_45_0' => 1, + 'phosh_notify_manager_get_default@@LIBPHOSH_0_45_0' => 1, + 'phosh_notify_manager_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_active@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_can_show_status@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_long_press_action_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_long_press_action_target@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_showing_status@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_status_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_active@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_can_show_status@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_long_press_action_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_long_press_action_target@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_showing_status@@LIBPHOSH_0_45_0' => 1, + 'phosh_quick_setting_set_status_page@@LIBPHOSH_0_45_0' => 1, + 'phosh_screenshot_manager_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_screenshot_manager_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_screenshot_manager_take_screenshot@@LIBPHOSH_0_45_0' => 1, + 'phosh_session_manager_inhibit@@LIBPHOSH_0_45_0' => 1, + 'phosh_session_manager_uninhibit@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_fade_out@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_default@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_launcher_entry_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_locked@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_lockscreen_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_lockscreen_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_monitor_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_mpris_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_primary_monitor@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_screenshot_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_session_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_usable_area@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_wifi_manager@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_get_wwan@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_shell_set_default@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_extra_widget@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_icon_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_icon_size@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_info@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_pixel_size@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_set_extra_widget@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_set_icon_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_set_icon_size@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_set_info@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_icon_set_pixel_size@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_get_content@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_get_footer@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_get_header@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_get_title@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_placeholder_get_icon_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_placeholder_get_title@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_placeholder_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_placeholder_set_icon_name@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_placeholder_set_title@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_set_content@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_set_footer@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_set_header@@LIBPHOSH_0_45_0' => 1, + 'phosh_status_page_set_title@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_append_to_strv@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_calculate_supported_mode_scales@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_data_uri_to_pixbuf@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_escape_markup@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_file_equal@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_gesture_is_touch@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_get_icon_by_wifi_strength@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_have_gnome_software@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_matches_app_info@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_open_mobile_settings_panel@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_open_settings_panel@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_remove_from_strv@@LIBPHOSH_0_45_0' => 1, + 'phosh_util_toggle_style_class@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_get_clock@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_get_default@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_get_type@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_local_date@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_new@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_set_default@@LIBPHOSH_0_45_0' => 1, + 'phosh_wall_clock_string_for_datetime@@LIBPHOSH_0_45_0' => 1, + 'phosh_wifi_manager_get_enabled@@LIBPHOSH_0_45_0' => 1, + 'phosh_wifi_manager_get_state@@LIBPHOSH_0_45_0' => 1, + 'phosh_wifi_manager_is_hotspot_master@@LIBPHOSH_0_45_0' => 1, + 'phosh_wifi_manager_set_hotspot_master@@LIBPHOSH_0_45_0' => 1, + 'phosh_wwan_has_data@@LIBPHOSH_0_45_0' => 1, + 'phosh_wwan_is_enabled@@LIBPHOSH_0_45_0' => 1, + 'phosh_wwan_is_unlocked@@LIBPHOSH_0_45_0' => 1, + 'phosh_wwan_set_data_enabled@@LIBPHOSH_0_45_0' => 1 + } + }, + 'Target' => 'unix', + 'TypeInfo' => { + '1' => { + 'Name' => 'void', + 'Type' => 'Intrinsic' + }, + '1043401' => { + 'BaseType' => '1043413', + 'Header' => 'gliststore.h', + 'Line' => '38', + 'Name' => 'GListStore', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1043413' => { + 'Name' => 'struct _GListStore', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1043430' => { + 'BaseType' => '1043401', + 'Name' => 'GListStore*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1057' => { + 'BaseType' => '1069', + 'Header' => 'gmain.h', + 'Line' => '87', + 'Name' => 'GSourcePrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1069' => { + 'Name' => 'struct _GSourcePrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1071186' => { + 'BaseType' => '1071198', + 'Header' => 'status-page.h', + 'Line' => '14', + 'Name' => 'PhoshStatusPage', + 'Size' => '48', + 'Type' => 'Typedef' + }, + '1071198' => { + 'Header' => 'status-page.h', + 'Line' => '14', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '960820' + } + }, + 'Name' => 'struct _PhoshStatusPage', + 'Size' => '48', + 'Type' => 'Struct' + }, + '1074' => { + 'BaseType' => '1086', + 'Header' => 'gmain.h', + 'Line' => '99', + 'Name' => 'GSourceCallbackFuncs', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '1086' => { + 'Header' => 'gmain.h', + 'Line' => '292', + 'Memb' => { + '0' => { + 'name' => 'ref', + 'offset' => '0', + 'type' => '406' + }, + '1' => { + 'name' => 'unref', + 'offset' => '8', + 'type' => '406' + }, + '2' => { + 'name' => 'get', + 'offset' => '22', + 'type' => '1343' + } + }, + 'Name' => 'struct _GSourceCallbackFuncs', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '1126198' => { + 'Header' => 'animation.h', + 'Line' => '30', + 'Memb' => { + '0' => { + 'name' => 'PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC', + 'value' => '0' + }, + '1' => { + 'name' => 'PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC', + 'value' => '1' + }, + '2' => { + 'name' => 'PHOSH_ANIMATION_TYPE_EASE_OUT_QUINTIC', + 'value' => '2' + }, + '3' => { + 'name' => 'PHOSH_ANIMATION_TYPE_EASE_OUT_BOUNCE', + 'value' => '3' + } + }, + 'Name' => 'enum PhoshAnimationType', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '1126210' => { + 'BaseType' => '1126222', + 'Header' => 'animation.h', + 'Line' => '32', + 'Name' => 'PhoshAnimation', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1126222' => { + 'Line' => '16', + 'Memb' => { + '0' => { + 'name' => 'ref_count', + 'offset' => '0', + 'type' => '2155560' + }, + '1' => { + 'name' => 'widget', + 'offset' => '8', + 'type' => '943522' + }, + '10' => { + 'name' => 'done_cb', + 'offset' => '128', + 'type' => '1126260' + }, + '11' => { + 'name' => 'user_data', + 'offset' => '136', + 'type' => '327' + }, + '2' => { + 'name' => 'value', + 'offset' => '22', + 'type' => '320' + }, + '3' => { + 'name' => 'value_from', + 'offset' => '36', + 'type' => '320' + }, + '4' => { + 'name' => 'value_to', + 'offset' => '50', + 'type' => '320' + }, + '5' => { + 'name' => 'duration', + 'offset' => '64', + 'type' => '140' + }, + '6' => { + 'name' => 'type', + 'offset' => '72', + 'type' => '1126198' + }, + '7' => { + 'name' => 'start_time', + 'offset' => '86', + 'type' => '140' + }, + '8' => { + 'name' => 'tick_cb_id', + 'offset' => '100', + 'type' => '277' + }, + '9' => { + 'name' => 'value_cb', + 'offset' => '114', + 'type' => '1126227' + } + }, + 'Name' => 'struct _PhoshAnimation', + 'PrivateABI' => 1, + 'Size' => '96', + 'Source' => 'animation.c', + 'Type' => 'Struct' + }, + '1126227' => { + 'BaseType' => '1126239', + 'Header' => 'animation.h', + 'Line' => '34', + 'Name' => 'PhoshAnimationValueCallback', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1126239' => { + 'Name' => 'void(*)(double, gpointer)', + 'Param' => { + '0' => { + 'type' => '320' + }, + '1' => { + 'type' => '327' + } + }, + 'Return' => '1', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1126260' => { + 'BaseType' => '406', + 'Header' => 'animation.h', + 'Line' => '36', + 'Name' => 'PhoshAnimationDoneCallback', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1126272' => { + 'BaseType' => '1126210', + 'Name' => 'PhoshAnimation*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1127280' => { + 'BaseType' => '1127292', + 'Header' => 'fader.h', + 'Line' => '16', + 'Name' => 'PhoshFader', + 'PrivateABI' => 1, + 'Size' => '88', + 'Type' => 'Typedef' + }, + '1127292' => { + 'Line' => '39', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '960916' + }, + '1' => { + 'name' => 'monitor', + 'offset' => '86', + 'type' => '961675' + }, + '2' => { + 'name' => 'style_class', + 'offset' => '100', + 'type' => '190' + }, + '3' => { + 'name' => 'fadeout', + 'offset' => '114', + 'type' => '1126272' + }, + '4' => { + 'name' => 'fade_out_time', + 'offset' => '128', + 'type' => '277' + }, + '5' => { + 'name' => 'fade_out_type', + 'offset' => '132', + 'type' => '1126198' + } + }, + 'Name' => 'struct _PhoshFader', + 'PrivateABI' => 1, + 'Size' => '88', + 'Source' => 'fader.c', + 'Type' => 'Struct' + }, + '1127420' => { + 'BaseType' => '1127280', + 'Name' => 'PhoshFader*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1132751' => { + 'Header' => 'gdesktop-enums.h', + 'Line' => '83', + 'Memb' => { + '0' => { + 'name' => 'G_DESKTOP_CLOCK_FORMAT_24H', + 'value' => '0' + }, + '1' => { + 'name' => 'G_DESKTOP_CLOCK_FORMAT_12H', + 'value' => '1' + } + }, + 'Name' => 'enum GDesktopClockFormat', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '1133087' => { + 'BaseType' => '1133099', + 'Header' => 'gdatetime.h', + 'Line' => '122', + 'Name' => 'GDateTime', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1133099' => { + 'Name' => 'struct _GDateTime', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1133268' => { + 'BaseType' => '1133087', + 'Name' => 'GDateTime*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1134863' => { + 'BaseType' => '1134875', + 'Header' => 'wall-clock.h', + 'Line' => '17', + 'Name' => 'PhoshWallClock', + 'Size' => '24', + 'Type' => 'Typedef' + }, + '1134875' => { + 'Header' => 'wall-clock.h', + 'Line' => '17', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + } + }, + 'Name' => 'struct _PhoshWallClock', + 'Size' => '24', + 'Type' => 'Struct' + }, + '1135071' => { + 'BaseType' => '1134863', + 'Name' => 'PhoshWallClock*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1143' => { + 'BaseType' => '1160', + 'Header' => 'gmain.h', + 'Line' => '158', + 'Name' => 'GSourceFuncs', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Typedef' + }, + '1155' => { + 'BaseType' => '1143', + 'Name' => 'GSourceFuncs const', + 'Size' => '48', + 'Type' => 'Const' + }, + '1158499' => { + 'BaseType' => '37359', + 'Header' => 'gstrfuncs.h', + 'Line' => '354', + 'Name' => 'GStrv', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1160' => { + 'Header' => 'gmain.h', + 'Line' => '399', + 'Memb' => { + '0' => { + 'name' => 'prepare', + 'offset' => '0', + 'type' => '1361' + }, + '1' => { + 'name' => 'check', + 'offset' => '8', + 'type' => '1404' + }, + '2' => { + 'name' => 'dispatch', + 'offset' => '22', + 'type' => '1437' + }, + '3' => { + 'name' => 'finalize', + 'offset' => '36', + 'type' => '1480' + }, + '4' => { + 'name' => 'closure_callback', + 'offset' => '50', + 'type' => '1259' + }, + '5' => { + 'name' => 'closure_marshal', + 'offset' => '64', + 'type' => '1348' + } + }, + 'Name' => 'struct _GSourceFuncs', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Struct' + }, + '1172435' => { + 'BaseType' => '1172447', + 'Header' => 'gtkbox.h', + 'Line' => '47', + 'Name' => 'GtkBox', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Typedef' + }, + '1172447' => { + 'Header' => 'gtkbox.h', + 'Line' => '51', + 'Memb' => { + '0' => { + 'name' => 'container', + 'offset' => '0', + 'type' => '960746' + }, + '1' => { + 'name' => 'priv', + 'offset' => '64', + 'type' => '1172504' + } + }, + 'Name' => 'struct _GtkBox', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Struct' + }, + '1172487' => { + 'BaseType' => '1172499', + 'Header' => 'gtkbox.h', + 'Line' => '48', + 'Name' => 'GtkBoxPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1172499' => { + 'Name' => 'struct _GtkBoxPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1172504' => { + 'BaseType' => '1172487', + 'Name' => 'GtkBoxPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '121' => { + 'BaseType' => '133', + 'Header' => 'glibconfig.h', + 'Line' => '57', + 'Name' => 'guint32', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '1220475' => { + 'BaseType' => '935031', + 'Name' => 'GdkRectangle const', + 'Size' => '16', + 'Type' => 'Const' + }, + '1221462' => { + 'BaseType' => '1220475', + 'Name' => 'GdkRectangle const*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1221467' => { + 'BaseType' => '935031', + 'Name' => 'GdkRectangle*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1259' => { + 'BaseType' => '754', + 'Header' => 'gmain.h', + 'Line' => '200', + 'Name' => 'GSourceFunc', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1271' => { + 'Name' => 'void(*)(GSource*)', + 'Param' => { + '0' => { + 'type' => '1287' + } + }, + 'Return' => '1', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1271215' => { + 'BaseType' => '1271232', + 'Header' => 'gdk-pixbuf-core.h', + 'Line' => '89', + 'Name' => 'GdkPixbuf', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1271232' => { + 'Name' => 'struct _GdkPixbuf', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1271237' => { + 'BaseType' => '1271215', + 'Name' => 'GdkPixbuf*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1277977' => { + 'BaseType' => '1277989', + 'Header' => 'background-image.h', + 'Line' => '17', + 'Name' => 'PhoshBackgroundImage', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1277989' => { + 'Line' => '30', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'file', + 'offset' => '36', + 'type' => '960325' + }, + '2' => { + 'name' => 'pixbuf', + 'offset' => '50', + 'type' => '1271237' + }, + '3' => { + 'name' => 'load_timer', + 'offset' => '64', + 'type' => '2202257' + } + }, + 'Name' => 'struct _PhoshBackgroundImage', + 'PrivateABI' => 1, + 'Size' => '48', + 'Source' => 'background-image.c', + 'Type' => 'Struct' + }, + '1277994' => { + 'BaseType' => '1277977', + 'Name' => 'PhoshBackgroundImage*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1287' => { + 'BaseType' => '848', + 'Name' => 'GSource*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1287258' => { + 'Header' => 'lockscreen.h', + 'Line' => '27', + 'Memb' => { + '0' => { + 'name' => 'PHOSH_LOCKSCREEN_PAGE_INFO', + 'value' => '0' + }, + '1' => { + 'name' => 'PHOSH_LOCKSCREEN_PAGE_EXTRA', + 'value' => '1' + }, + '2' => { + 'name' => 'PHOSH_LOCKSCREEN_PAGE_UNLOCK', + 'value' => '2' + } + }, + 'Name' => 'enum PhoshLockscreenPage', + 'Size' => '4', + 'Type' => 'Enum' + }, + '1287270' => { + 'BaseType' => '1287282', + 'Header' => 'lockscreen.h', + 'Line' => '31', + 'Name' => 'PhoshLockscreen', + 'Size' => '56', + 'Type' => 'Typedef' + }, + '1287282' => { + 'Header' => 'lockscreen.h', + 'Line' => '31', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '960916' + } + }, + 'Name' => 'struct _PhoshLockscreen', + 'Size' => '56', + 'Type' => 'Struct' + }, + '1287309' => { + 'BaseType' => '1287270', + 'Name' => 'PhoshLockscreen*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1287314' => { + 'BaseType' => '1287326', + 'Header' => 'lockscreen-manager.h', + 'Line' => '14', + 'Name' => 'PhoshLockscreenManager', + 'Size' => '112', + 'Type' => 'Typedef' + }, + '1287326' => { + 'Line' => '56', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'lockscreen', + 'offset' => '36', + 'type' => '1287309' + }, + '10' => { + 'name' => 'locking', + 'offset' => '146', + 'type' => '253' + }, + '11' => { + 'name' => 'active_time', + 'offset' => '150', + 'type' => '140' + }, + '12' => { + 'name' => 'calls_manager', + 'offset' => '260', + 'type' => '1287571' + }, + '2' => { + 'name' => 'shields', + 'offset' => '50', + 'type' => '933217' + }, + '3' => { + 'name' => 'bg_settings', + 'offset' => '64', + 'type' => '960396' + }, + '4' => { + 'name' => 'bg_file', + 'offset' => '72', + 'type' => '960325' + }, + '5' => { + 'name' => 'bg_file_monitor', + 'offset' => '86', + 'type' => '960352' + }, + '6' => { + 'name' => 'bg_style', + 'offset' => '100', + 'type' => '961736' + }, + '7' => { + 'name' => 'cached_bg_image', + 'offset' => '114', + 'type' => '1277994' + }, + '8' => { + 'name' => 'bg_load_cancel', + 'offset' => '128', + 'type' => '4850' + }, + '9' => { + 'name' => 'locked', + 'offset' => '136', + 'type' => '253' + } + }, + 'Name' => 'struct _PhoshLockscreenManager', + 'PrivateABI' => 1, + 'Size' => '112', + 'Source' => 'lockscreen-manager.c', + 'Type' => 'Struct' + }, + '1287544' => { + 'BaseType' => '1287314', + 'Name' => 'PhoshLockscreenManager*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1287554' => { + 'BaseType' => '1287566', + 'Header' => 'calls-manager.h', + 'Line' => '36', + 'Name' => 'PhoshCallsManager', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1287566' => { + 'Line' => '51', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '961897' + }, + '1' => { + 'name' => 'present', + 'offset' => '36', + 'type' => '253' + }, + '2' => { + 'name' => 'incoming', + 'offset' => '40', + 'type' => '253' + }, + '3' => { + 'name' => 'active_call', + 'offset' => '50', + 'type' => '190' + }, + '4' => { + 'name' => 'om_client', + 'offset' => '64', + 'type' => '241203' + }, + '5' => { + 'name' => 'cancel', + 'offset' => '72', + 'type' => '4850' + }, + '6' => { + 'name' => 'calls', + 'offset' => '86', + 'type' => '1846' + }, + '7' => { + 'name' => 'calls_store', + 'offset' => '100', + 'type' => '1043430' + } + }, + 'Name' => 'struct _PhoshCallsManager', + 'PrivateABI' => 1, + 'Size' => '72', + 'Source' => 'calls-manager.c', + 'Type' => 'Struct' + }, + '1287571' => { + 'BaseType' => '1287554', + 'Name' => 'PhoshCallsManager*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1292' => { + 'BaseType' => '1074', + 'Name' => 'GSourceCallbackFuncs*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1297' => { + 'BaseType' => '1155', + 'Name' => 'GSourceFuncs const*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1302' => { + 'BaseType' => '831', + 'Name' => 'GMainContext*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1307' => { + 'BaseType' => '1057', + 'Name' => 'GSourcePrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '133' => { + 'Name' => 'unsigned int', + 'Size' => '4', + 'Type' => 'Intrinsic' + }, + '1338' => { + 'BaseType' => '1259', + 'Name' => 'GSourceFunc*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1343' => { + 'Name' => 'void(*)(gpointer, GSource*, GSourceFunc*, gpointer*)', + 'Param' => { + '0' => { + 'type' => '327' + }, + '1' => { + 'type' => '1287' + }, + '2' => { + 'type' => '1338' + }, + '3' => { + 'type' => '490' + } + }, + 'Return' => '1', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1348' => { + 'BaseType' => '577', + 'Header' => 'gmain.h', + 'Line' => '308', + 'Name' => 'GSourceDummyMarshal', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1355691' => { + 'BaseType' => '60', + 'Name' => 'int*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1361' => { + 'BaseType' => '1374', + 'Header' => 'gmain.h', + 'Line' => '334', + 'Name' => 'GSourceFuncsPrepareFunc', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1374' => { + 'Name' => 'gboolean(*)(GSource*, gint*)', + 'Param' => { + '0' => { + 'type' => '1287' + }, + '1' => { + 'type' => '1399' + } + }, + 'Return' => '253', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1392314' => { + 'BaseType' => '1392326', + 'Header' => 'gstring.h', + 'Line' => '43', + 'Name' => 'GString', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '1392326' => { + 'Header' => 'gstring.h', + 'Line' => '45', + 'Memb' => { + '0' => { + 'name' => 'str', + 'offset' => '0', + 'type' => '485' + }, + '1' => { + 'name' => 'len', + 'offset' => '8', + 'type' => '164' + }, + '2' => { + 'name' => 'allocated_len', + 'offset' => '22', + 'type' => '164' + } + }, + 'Name' => 'struct _GString', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '1392379' => { + 'BaseType' => '1392314', + 'Name' => 'GString*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1394639' => { + 'BaseType' => '1394651', + 'Header' => 'giotypes.h', + 'Line' => '97', + 'Name' => 'GInputStream', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '1394651' => { + 'Header' => 'ginputstream.h', + 'Line' => '44', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '1394823' + } + }, + 'Name' => 'struct _GInputStream', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '1394796' => { + 'BaseType' => '1394639', + 'Name' => 'GInputStream*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1394806' => { + 'BaseType' => '1394818', + 'Header' => 'ginputstream.h', + 'Line' => '42', + 'Name' => 'GInputStreamPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '1394818' => { + 'Name' => 'struct _GInputStreamPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1394823' => { + 'BaseType' => '1394806', + 'Name' => 'GInputStreamPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1399' => { + 'BaseType' => '241', + 'Name' => 'gint*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '140' => { + 'BaseType' => '46', + 'Header' => 'glibconfig.h', + 'Line' => '66', + 'Name' => 'gint64', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1404' => { + 'BaseType' => '1417', + 'Header' => 'gmain.h', + 'Line' => '355', + 'Name' => 'GSourceFuncsCheckFunc', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1417' => { + 'Name' => 'gboolean(*)(GSource*)', + 'Param' => { + '0' => { + 'type' => '1287' + } + }, + 'Return' => '253', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1437' => { + 'BaseType' => '1450', + 'Header' => 'gmain.h', + 'Line' => '380', + 'Name' => 'GSourceFuncsDispatchFunc', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1450' => { + 'Name' => 'gboolean(*)(GSource*, GSourceFunc, gpointer)', + 'Param' => { + '0' => { + 'type' => '1287' + }, + '1' => { + 'type' => '1259' + }, + '2' => { + 'type' => '327' + } + }, + 'Return' => '253', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '1480' => { + 'BaseType' => '1271', + 'Header' => 'gmain.h', + 'Line' => '397', + 'Name' => 'GSourceFuncsFinalizeFunc', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '152' => { + 'BaseType' => '53', + 'Header' => 'glibconfig.h', + 'Line' => '67', + 'Name' => 'guint64', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1632853' => { + 'BaseType' => '1632865', + 'Header' => 'screenshot-manager.h', + 'Line' => '17', + 'Name' => 'PhoshScreenshotManager', + 'Size' => '136', + 'Type' => 'Typedef' + }, + '1632865' => { + 'Line' => '85', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '801922' + }, + '1' => { + 'name' => 'dbus_name_id', + 'offset' => '64', + 'type' => '60' + }, + '10' => { + 'name' => 'action_names', + 'offset' => '274', + 'type' => '1158499' + }, + '11' => { + 'name' => 'settings', + 'offset' => '288', + 'type' => '960396' + }, + '12' => { + 'name' => 'cancel', + 'offset' => '296', + 'type' => '4850' + }, + '2' => { + 'name' => 'wl_scm', + 'offset' => '72', + 'type' => '1633709' + }, + '3' => { + 'name' => 'frames', + 'offset' => '86', + 'type' => '1633714' + }, + '4' => { + 'name' => 'slurp', + 'offset' => '100', + 'type' => '1633719' + }, + '5' => { + 'name' => 'fader', + 'offset' => '114', + 'type' => '1127420' + }, + '6' => { + 'name' => 'fader_id', + 'offset' => '128', + 'type' => '277' + }, + '7' => { + 'name' => 'opaque', + 'offset' => '136', + 'type' => '1127420' + }, + '8' => { + 'name' => 'opaque_id', + 'offset' => '150', + 'type' => '277' + }, + '9' => { + 'name' => 'for_clipboard', + 'offset' => '260', + 'type' => '1271237' + } + }, + 'Name' => 'struct _PhoshScreenshotManager', + 'PrivateABI' => 1, + 'Size' => '136', + 'Source' => 'screenshot-manager.c', + 'Type' => 'Struct' + }, + '1633096' => { + 'BaseType' => '1632853', + 'Name' => 'PhoshScreenshotManager*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1633564' => { + 'Line' => '72', + 'Memb' => { + '0' => { + 'name' => 'frames', + 'offset' => '0', + 'type' => '732' + }, + '1' => { + 'name' => 'invocation', + 'offset' => '8', + 'type' => '6022' + }, + '2' => { + 'name' => 'flash', + 'offset' => '22', + 'type' => '253' + }, + '3' => { + 'name' => 'filename', + 'offset' => '36', + 'type' => '190' + }, + '4' => { + 'name' => 'num_outputs', + 'offset' => '50', + 'type' => '277' + }, + '5' => { + 'name' => 'max_scale', + 'offset' => '54', + 'type' => '301' + }, + '6' => { + 'name' => 'area', + 'offset' => '64', + 'type' => '1221467' + }, + '7' => { + 'name' => 'copy_to_clipboard', + 'offset' => '72', + 'type' => '253' + } + }, + 'Name' => 'struct ScreencopyFrames', + 'PrivateABI' => 1, + 'Size' => '56', + 'Source' => 'screenshot-manager.c', + 'Type' => 'Struct' + }, + '1633676' => { + 'BaseType' => '195', + 'Name' => 'char[64]', + 'Size' => '64', + 'Type' => 'Array' + }, + '1633692' => { + 'Line' => '82', + 'Memb' => { + '0' => { + 'name' => 'child_watch_id', + 'offset' => '0', + 'type' => '277' + }, + '1' => { + 'name' => 'pid', + 'offset' => '4', + 'type' => '910469' + }, + '2' => { + 'name' => 'invocation', + 'offset' => '8', + 'type' => '6022' + }, + '3' => { + 'name' => 'stdout_', + 'offset' => '22', + 'type' => '1394796' + }, + '4' => { + 'name' => 'cancel', + 'offset' => '36', + 'type' => '4850' + }, + '5' => { + 'name' => 'read_buf', + 'offset' => '50', + 'type' => '1633676' + }, + '6' => { + 'name' => 'response', + 'offset' => '150', + 'type' => '1392379' + } + }, + 'Name' => 'struct SlurpArea', + 'PrivateABI' => 1, + 'Size' => '104', + 'Source' => 'screenshot-manager.c', + 'Type' => 'Struct' + }, + '1633704' => { + 'Name' => 'struct zwlr_screencopy_manager_v1', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '1633709' => { + 'BaseType' => '1633704', + 'Name' => 'struct zwlr_screencopy_manager_v1*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1633714' => { + 'BaseType' => '1633564', + 'Name' => 'ScreencopyFrames*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1633719' => { + 'BaseType' => '1633692', + 'Name' => 'SlurpArea*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '164' => { + 'BaseType' => '53', + 'Header' => 'glibconfig.h', + 'Line' => '83', + 'Name' => 'gsize', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1841' => { + 'BaseType' => '650', + 'Name' => 'GData*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1846' => { + 'BaseType' => '737', + 'Name' => 'GHashTable*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1866' => { + 'BaseType' => '164', + 'Header' => 'gtype.h', + 'Line' => '427', + 'Name' => 'GType', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '1879' => { + 'BaseType' => '1897', + 'Header' => 'gtype.h', + 'Line' => '431', + 'Name' => 'GValue', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '188' => { + 'BaseType' => '1', + 'Name' => 'void*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1897' => { + 'Header' => 'gvalue.h', + 'Line' => '131', + 'Memb' => { + '0' => { + 'name' => 'g_type', + 'offset' => '0', + 'type' => '1866' + }, + '1' => { + 'name' => 'data', + 'offset' => '8', + 'type' => '2412' + } + }, + 'Name' => 'struct _GValue', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '190' => { + 'BaseType' => '195', + 'Name' => 'char*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '1937' => { + 'BaseType' => '1950', + 'Header' => 'gtype.h', + 'Line' => '434', + 'Name' => 'GTypeClass', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '195' => { + 'Name' => 'char', + 'Size' => '1', + 'Type' => 'Intrinsic' + }, + '1950' => { + 'Header' => 'gtype.h', + 'Line' => '451', + 'Memb' => { + '0' => { + 'name' => 'g_type', + 'offset' => '0', + 'type' => '1866' + } + }, + 'Name' => 'struct _GTypeClass', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Struct' + }, + '202' => { + 'BaseType' => '195', + 'Name' => 'char const', + 'Size' => '1', + 'Type' => 'Const' + }, + '2035' => { + 'BaseType' => '2048', + 'Header' => 'gtype.h', + 'Line' => '436', + 'Name' => 'GTypeInstance', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '2040796' => { + 'Header' => 'gtkenums.h', + 'Line' => '197', + 'Memb' => { + '0' => { + 'name' => 'GTK_ICON_SIZE_INVALID', + 'value' => '0' + }, + '1' => { + 'name' => 'GTK_ICON_SIZE_MENU', + 'value' => '1' + }, + '2' => { + 'name' => 'GTK_ICON_SIZE_SMALL_TOOLBAR', + 'value' => '2' + }, + '3' => { + 'name' => 'GTK_ICON_SIZE_LARGE_TOOLBAR', + 'value' => '3' + }, + '4' => { + 'name' => 'GTK_ICON_SIZE_BUTTON', + 'value' => '4' + }, + '5' => { + 'name' => 'GTK_ICON_SIZE_DND', + 'value' => '5' + }, + '6' => { + 'name' => 'GTK_ICON_SIZE_DIALOG', + 'value' => '6' + } + }, + 'Name' => 'enum GtkIconSize', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '2048' => { + 'Header' => 'gtype.h', + 'Line' => '461', + 'Memb' => { + '0' => { + 'name' => 'g_class', + 'offset' => '0', + 'type' => '2152' + } + }, + 'Name' => 'struct _GTypeInstance', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Struct' + }, + '207' => { + 'BaseType' => '202', + 'Name' => 'char const*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '212' => { + 'BaseType' => '195', + 'Header' => 'gtypes.h', + 'Line' => '52', + 'Name' => 'gchar', + 'PrivateABI' => 1, + 'Size' => '1', + 'Type' => 'Typedef' + }, + '2152' => { + 'BaseType' => '1937', + 'Name' => 'GTypeClass*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '2155560' => { + 'BaseType' => '241', + 'Header' => 'gtypes.h', + 'Line' => '587', + 'Name' => 'gatomicrefcount', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '2202240' => { + 'BaseType' => '2202252', + 'Header' => 'gtimer.h', + 'Line' => '42', + 'Name' => 'GTimer', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '2202252' => { + 'Name' => 'struct _GTimer', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '2202257' => { + 'BaseType' => '2202240', + 'Name' => 'GTimer*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '229' => { + 'BaseType' => '46', + 'Header' => 'gtypes.h', + 'Line' => '54', + 'Name' => 'glong', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '2293' => { + 'BaseType' => '1879', + 'Name' => 'GValue*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '2303' => { + 'Header' => 'gvalue.h', + 'Line' => '137', + 'Memb' => { + '0' => { + 'name' => 'v_int', + 'offset' => '0', + 'type' => '241' + }, + '1' => { + 'name' => 'v_uint', + 'offset' => '0', + 'type' => '277' + }, + '2' => { + 'name' => 'v_long', + 'offset' => '0', + 'type' => '229' + }, + '3' => { + 'name' => 'v_ulong', + 'offset' => '0', + 'type' => '265' + }, + '4' => { + 'name' => 'v_int64', + 'offset' => '0', + 'type' => '140' + }, + '5' => { + 'name' => 'v_uint64', + 'offset' => '0', + 'type' => '152' + }, + '6' => { + 'name' => 'v_float', + 'offset' => '0', + 'type' => '289' + }, + '7' => { + 'name' => 'v_double', + 'offset' => '0', + 'type' => '308' + }, + '8' => { + 'name' => 'v_pointer', + 'offset' => '0', + 'type' => '327' + } + }, + 'Name' => 'anon-union-gvalue.h-137', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Union' + }, + '241' => { + 'BaseType' => '60', + 'Header' => 'gtypes.h', + 'Line' => '55', + 'Name' => 'gint', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '241082' => { + 'BaseType' => '241095', + 'Header' => 'calls-dbus.h', + 'Line' => '374', + 'Name' => 'PhoshDBusObjectManagerClient', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Typedef' + }, + '241095' => { + 'Header' => 'calls-dbus.h', + 'Line' => '378', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '5863' + }, + '1' => { + 'name' => 'priv', + 'offset' => '50', + 'type' => '241198' + } + }, + 'Name' => 'struct _PhoshDBusObjectManagerClient', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Struct' + }, + '241180' => { + 'BaseType' => '241193', + 'Header' => 'calls-dbus.h', + 'Line' => '376', + 'Name' => 'PhoshDBusObjectManagerClientPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '241193' => { + 'Name' => 'struct _PhoshDBusObjectManagerClientPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '241198' => { + 'BaseType' => '241180', + 'Name' => 'PhoshDBusObjectManagerClientPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '2412' => { + 'BaseType' => '2303', + 'Name' => 'anon-union-gvalue.h-137[2]', + 'Size' => '16', + 'Type' => 'Array' + }, + '241203' => { + 'BaseType' => '241082', + 'Name' => 'PhoshDBusObjectManagerClient*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '253' => { + 'BaseType' => '241', + 'Header' => 'gtypes.h', + 'Line' => '56', + 'Name' => 'gboolean', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '265' => { + 'BaseType' => '53', + 'Header' => 'gtypes.h', + 'Line' => '60', + 'Name' => 'gulong', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '277' => { + 'BaseType' => '133', + 'Header' => 'gtypes.h', + 'Line' => '61', + 'Name' => 'guint', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '289' => { + 'BaseType' => '301', + 'Header' => 'gtypes.h', + 'Line' => '63', + 'Name' => 'gfloat', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '2991339' => { + 'BaseType' => '1071186', + 'Name' => 'PhoshStatusPage*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '2991344' => { + 'BaseType' => '2991356', + 'Header' => 'quick-setting.h', + 'Line' => '16', + 'Name' => 'PhoshQuickSetting', + 'Size' => '48', + 'Type' => 'Typedef' + }, + '2991356' => { + 'Header' => 'quick-setting.h', + 'Line' => '16', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '1172435' + } + }, + 'Name' => 'struct _PhoshQuickSetting', + 'Size' => '48', + 'Type' => 'Struct' + }, + '2991562' => { + 'BaseType' => '2991344', + 'Name' => 'PhoshQuickSetting*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '301' => { + 'Name' => 'float', + 'Size' => '4', + 'Type' => 'Intrinsic' + }, + '308' => { + 'BaseType' => '320', + 'Header' => 'gtypes.h', + 'Line' => '64', + 'Name' => 'gdouble', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '320' => { + 'Name' => 'double', + 'Size' => '8', + 'Type' => 'Intrinsic' + }, + '327' => { + 'BaseType' => '188', + 'Header' => 'gtypes.h', + 'Line' => '109', + 'Name' => 'gpointer', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '3321' => { + 'BaseType' => '3333', + 'Header' => 'gobject.h', + 'Line' => '192', + 'Name' => 'GObject', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '3333' => { + 'Header' => 'gobject.h', + 'Line' => '252', + 'Memb' => { + '0' => { + 'name' => 'g_type_instance', + 'offset' => '0', + 'type' => '2035' + }, + '1' => { + 'name' => 'ref_count', + 'offset' => '8', + 'type' => '277' + }, + '2' => { + 'name' => 'qdata', + 'offset' => '22', + 'type' => '1841' + } + }, + 'Name' => 'struct _GObject', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '37359' => { + 'BaseType' => '485', + 'Name' => 'gchar**', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '406' => { + 'Name' => 'void(*)(gpointer)', + 'Param' => { + '0' => { + 'type' => '327' + } + }, + 'Return' => '1', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '46' => { + 'Name' => 'long', + 'Size' => '8', + 'Type' => 'Intrinsic' + }, + '4738' => { + 'BaseType' => '4750', + 'Header' => 'giotypes.h', + 'Line' => '40', + 'Name' => 'GCancellable', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '4750' => { + 'Header' => 'gcancellable.h', + 'Line' => '44', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '5953' + } + }, + 'Name' => 'struct _GCancellable', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '485' => { + 'BaseType' => '212', + 'Name' => 'gchar*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '4850' => { + 'BaseType' => '4738', + 'Name' => 'GCancellable*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '490' => { + 'BaseType' => '327', + 'Name' => 'gpointer*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '5002' => { + 'BaseType' => '5015', + 'Header' => 'giotypes.h', + 'Line' => '470', + 'Name' => 'GDBusMethodInvocation', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '5015' => { + 'Name' => 'struct _GDBusMethodInvocation', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '53' => { + 'Name' => 'unsigned long', + 'Size' => '8', + 'Type' => 'Intrinsic' + }, + '5668' => { + 'BaseType' => '5681', + 'Header' => 'giotypes.h', + 'Line' => '516', + 'Name' => 'GDBusInterfaceSkeleton', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '5681' => { + 'Header' => 'gdbusinterfaceskeleton.h', + 'Line' => '40', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '6325' + } + }, + 'Name' => 'struct _GDBusInterfaceSkeleton', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '577' => { + 'Name' => 'void(*)()', + 'Return' => '1', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '5863' => { + 'BaseType' => '5876', + 'Header' => 'giotypes.h', + 'Line' => '521', + 'Name' => 'GDBusObjectManagerClient', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '5876' => { + 'Header' => 'gdbusobjectmanagerclient.h', + 'Line' => '40', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '6781' + } + }, + 'Name' => 'struct _GDBusObjectManagerClient', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '590' => { + 'BaseType' => '602', + 'Header' => 'gthread.h', + 'Line' => '53', + 'Name' => 'GMutex', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Typedef' + }, + '5936' => { + 'BaseType' => '5948', + 'Header' => 'gcancellable.h', + 'Line' => '42', + 'Name' => 'GCancellablePrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '5948' => { + 'Name' => 'struct _GCancellablePrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '5953' => { + 'BaseType' => '5936', + 'Name' => 'GCancellablePrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '60' => { + 'Name' => 'int', + 'Size' => '4', + 'Type' => 'Intrinsic' + }, + '602' => { + 'Header' => 'gthread.h', + 'Line' => '60', + 'Memb' => { + '0' => { + 'name' => 'p', + 'offset' => '0', + 'type' => '327' + }, + '1' => { + 'name' => 'i', + 'offset' => '0', + 'type' => '634' + } + }, + 'Name' => 'union _GMutex', + 'PrivateABI' => 1, + 'Size' => '8', + 'Type' => 'Union' + }, + '6022' => { + 'BaseType' => '5002', + 'Name' => 'GDBusMethodInvocation*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '6308' => { + 'BaseType' => '6320', + 'Header' => 'gdbusinterfaceskeleton.h', + 'Line' => '38', + 'Name' => 'GDBusInterfaceSkeletonPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '6320' => { + 'Name' => 'struct _GDBusInterfaceSkeletonPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '6325' => { + 'BaseType' => '6308', + 'Name' => 'GDBusInterfaceSkeletonPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '634' => { + 'BaseType' => '277', + 'Name' => 'guint[2]', + 'Size' => '8', + 'Type' => 'Array' + }, + '650' => { + 'BaseType' => '662', + 'Header' => 'gdataset.h', + 'Line' => '38', + 'Name' => 'GData', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '662' => { + 'Name' => 'struct _GData', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '667' => { + 'BaseType' => '679', + 'Header' => 'glist.h', + 'Line' => '39', + 'Name' => 'GList', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '6764' => { + 'BaseType' => '6776', + 'Header' => 'gdbusobjectmanagerclient.h', + 'Line' => '38', + 'Name' => 'GDBusObjectManagerClientPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '6776' => { + 'Name' => 'struct _GDBusObjectManagerClientPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '6781' => { + 'BaseType' => '6764', + 'Name' => 'GDBusObjectManagerClientPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '679' => { + 'Header' => 'glist.h', + 'Line' => '41', + 'Memb' => { + '0' => { + 'name' => 'data', + 'offset' => '0', + 'type' => '327' + }, + '1' => { + 'name' => 'next', + 'offset' => '8', + 'type' => '732' + }, + '2' => { + 'name' => 'prev', + 'offset' => '22', + 'type' => '732' + } + }, + 'Name' => 'struct _GList', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '693750' => { + 'BaseType' => '60', + 'Header' => 'glibconfig.h', + 'Line' => '56', + 'Name' => 'gint32', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '732' => { + 'BaseType' => '667', + 'Name' => 'GList*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '737' => { + 'BaseType' => '749', + 'Header' => 'ghash.h', + 'Line' => '40', + 'Name' => 'GHashTable', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '749' => { + 'Name' => 'struct _GHashTable', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '754' => { + 'Name' => 'gboolean(*)(gpointer)', + 'Param' => { + '0' => { + 'type' => '327' + } + }, + 'Return' => '253', + 'Size' => '8', + 'Type' => 'FuncPtr' + }, + '774' => { + 'BaseType' => '786', + 'Header' => 'gslist.h', + 'Line' => '39', + 'Name' => 'GSList', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Typedef' + }, + '786' => { + 'Header' => 'gslist.h', + 'Line' => '41', + 'Memb' => { + '0' => { + 'name' => 'data', + 'offset' => '0', + 'type' => '327' + }, + '1' => { + 'name' => 'next', + 'offset' => '8', + 'type' => '826' + } + }, + 'Name' => 'struct _GSList', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Struct' + }, + '801922' => { + 'BaseType' => '801935', + 'Header' => 'phosh-screenshot-dbus.h', + 'Line' => '358', + 'Name' => 'PhoshDBusScreenshotSkeleton', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Typedef' + }, + '801935' => { + 'Header' => 'phosh-screenshot-dbus.h', + 'Line' => '362', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '5668' + }, + '1' => { + 'name' => 'priv', + 'offset' => '50', + 'type' => '802117' + } + }, + 'Name' => 'struct _PhoshDBusScreenshotSkeleton', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Struct' + }, + '802019' => { + 'BaseType' => '802032', + 'Header' => 'phosh-screenshot-dbus.h', + 'Line' => '360', + 'Name' => 'PhoshDBusScreenshotSkeletonPrivate', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Typedef' + }, + '802032' => { + 'Line' => '2453', + 'Memb' => { + '0' => { + 'name' => 'properties', + 'offset' => '0', + 'type' => '2293' + }, + '1' => { + 'name' => 'changed_properties', + 'offset' => '8', + 'type' => '732' + }, + '2' => { + 'name' => 'changed_properties_idle_source', + 'offset' => '22', + 'type' => '1287' + }, + '3' => { + 'name' => 'context', + 'offset' => '36', + 'type' => '1302' + }, + '4' => { + 'name' => 'lock', + 'offset' => '50', + 'type' => '590' + } + }, + 'Name' => 'struct _PhoshDBusScreenshotSkeletonPrivate', + 'PrivateABI' => 1, + 'Size' => '40', + 'Source' => 'phosh-screenshot-dbus.c', + 'Type' => 'Struct' + }, + '802117' => { + 'BaseType' => '802019', + 'Name' => 'PhoshDBusScreenshotSkeletonPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '826' => { + 'BaseType' => '774', + 'Name' => 'GSList*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '831' => { + 'BaseType' => '843', + 'Header' => 'gmain.h', + 'Line' => '70', + 'Name' => 'GMainContext', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '843' => { + 'Name' => 'struct _GMainContext', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '848' => { + 'BaseType' => '860', + 'Header' => 'gmain.h', + 'Line' => '86', + 'Name' => 'GSource', + 'PrivateABI' => 1, + 'Size' => '96', + 'Type' => 'Typedef' + }, + '860' => { + 'Header' => 'gmain.h', + 'Line' => '267', + 'Memb' => { + '0' => { + 'name' => 'callback_data', + 'offset' => '0', + 'type' => '327' + }, + '1' => { + 'name' => 'callback_funcs', + 'offset' => '8', + 'type' => '1292' + }, + '10' => { + 'name' => 'next', + 'offset' => '114', + 'type' => '1287' + }, + '11' => { + 'name' => 'name', + 'offset' => '128', + 'type' => '190' + }, + '12' => { + 'name' => 'priv', + 'offset' => '136', + 'type' => '1307' + }, + '2' => { + 'name' => 'source_funcs', + 'offset' => '22', + 'type' => '1297' + }, + '3' => { + 'name' => 'ref_count', + 'offset' => '36', + 'type' => '277' + }, + '4' => { + 'name' => 'context', + 'offset' => '50', + 'type' => '1302' + }, + '5' => { + 'name' => 'priority', + 'offset' => '64', + 'type' => '241' + }, + '6' => { + 'name' => 'flags', + 'offset' => '68', + 'type' => '277' + }, + '7' => { + 'name' => 'source_id', + 'offset' => '72', + 'type' => '277' + }, + '8' => { + 'name' => 'poll_fds', + 'offset' => '86', + 'type' => '826' + }, + '9' => { + 'name' => 'prev', + 'offset' => '100', + 'type' => '1287' + } + }, + 'Name' => 'struct _GSource', + 'PrivateABI' => 1, + 'Size' => '96', + 'Type' => 'Struct' + }, + '910469' => { + 'BaseType' => '60', + 'Header' => 'glibconfig.h', + 'Line' => '201', + 'Name' => 'GPid', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '915101' => { + 'BaseType' => '915113', + 'Header' => 'shell.h', + 'Line' => '20', + 'Name' => 'PhoshShell', + 'Size' => '24', + 'Type' => 'Typedef' + }, + '915113' => { + 'Header' => 'shell.h', + 'Line' => '20', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + } + }, + 'Name' => 'struct _PhoshShell', + 'Size' => '24', + 'Type' => 'Struct' + }, + '915140' => { + 'BaseType' => '915101', + 'Name' => 'PhoshShell*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '932748' => { + 'BaseType' => '932760', + 'Header' => 'garray.h', + 'Line' => '39', + 'Name' => 'GArray', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Typedef' + }, + '932760' => { + 'Header' => 'garray.h', + 'Line' => '43', + 'Memb' => { + '0' => { + 'name' => 'data', + 'offset' => '0', + 'type' => '485' + }, + '1' => { + 'name' => 'len', + 'offset' => '8', + 'type' => '277' + } + }, + 'Name' => 'struct _GArray', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Struct' + }, + '932800' => { + 'BaseType' => '932812', + 'Header' => 'garray.h', + 'Line' => '41', + 'Name' => 'GPtrArray', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Typedef' + }, + '932812' => { + 'Header' => 'garray.h', + 'Line' => '55', + 'Memb' => { + '0' => { + 'name' => 'pdata', + 'offset' => '0', + 'type' => '490' + }, + '1' => { + 'name' => 'len', + 'offset' => '8', + 'type' => '277' + } + }, + 'Name' => 'struct _GPtrArray', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Struct' + }, + '933212' => { + 'BaseType' => '932748', + 'Name' => 'GArray*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '933217' => { + 'BaseType' => '932800', + 'Name' => 'GPtrArray*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '934218' => { + 'BaseType' => '3333', + 'Header' => 'gobject.h', + 'Line' => '194', + 'Name' => 'GInitiallyUnowned', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '934867' => { + 'Header' => 'cairo.h', + 'Line' => '532', + 'Memb' => { + '0' => { + 'name' => 'x', + 'offset' => '0', + 'type' => '60' + }, + '1' => { + 'name' => 'y', + 'offset' => '4', + 'type' => '60' + }, + '2' => { + 'name' => 'width', + 'offset' => '8', + 'type' => '60' + }, + '3' => { + 'name' => 'height', + 'offset' => '18', + 'type' => '60' + } + }, + 'Name' => 'struct _cairo_rectangle_int', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Struct' + }, + '934934' => { + 'BaseType' => '934867', + 'Header' => 'cairo.h', + 'Line' => '535', + 'Name' => 'cairo_rectangle_int_t', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Typedef' + }, + '935031' => { + 'BaseType' => '934934', + 'Header' => 'gdktypes.h', + 'Line' => '93', + 'Name' => 'GdkRectangle', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Typedef' + }, + '941062' => { + 'BaseType' => '941074', + 'Header' => 'gtktypes.h', + 'Line' => '46', + 'Name' => 'GtkWidget', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '941074' => { + 'Header' => 'gtkwidget.h', + 'Line' => '133', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '934218' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '943527' + } + }, + 'Name' => 'struct _GtkWidget', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '942230' => { + 'BaseType' => '942242', + 'Header' => 'gtkwidget.h', + 'Line' => '66', + 'Name' => 'GtkWidgetPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '942242' => { + 'Name' => 'struct _GtkWidgetPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '943522' => { + 'BaseType' => '941062', + 'Name' => 'GtkWidget*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '943527' => { + 'BaseType' => '942230', + 'Name' => 'GtkWidgetPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960124' => { + 'BaseType' => '960136', + 'Header' => 'giotypes.h', + 'Line' => '62', + 'Name' => 'GSettings', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '960136' => { + 'Header' => 'gsettings.h', + 'Line' => '65', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '960401' + } + }, + 'Name' => 'struct _GSettings', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '960176' => { + 'BaseType' => '960188', + 'Header' => 'giotypes.h', + 'Line' => '70', + 'Name' => 'GFileMonitor', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Typedef' + }, + '960188' => { + 'Header' => 'gfilemonitor.h', + 'Line' => '44', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'priv', + 'offset' => '36', + 'type' => '960374' + } + }, + 'Name' => 'struct _GFileMonitor', + 'PrivateABI' => 1, + 'Size' => '32', + 'Type' => 'Struct' + }, + '960228' => { + 'BaseType' => '960240', + 'Header' => 'giotypes.h', + 'Line' => '74', + 'Name' => 'GFile', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960240' => { + 'Name' => 'struct _GFile', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960325' => { + 'BaseType' => '960228', + 'Name' => 'GFile*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960352' => { + 'BaseType' => '960176', + 'Name' => 'GFileMonitor*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960357' => { + 'BaseType' => '960369', + 'Header' => 'gfilemonitor.h', + 'Line' => '42', + 'Name' => 'GFileMonitorPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960369' => { + 'Name' => 'struct _GFileMonitorPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960374' => { + 'BaseType' => '960357', + 'Name' => 'GFileMonitorPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960379' => { + 'BaseType' => '960391', + 'Header' => 'gsettings.h', + 'Line' => '44', + 'Name' => 'GSettingsPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960391' => { + 'Name' => 'struct _GSettingsPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960396' => { + 'BaseType' => '960124', + 'Name' => 'GSettings*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960401' => { + 'BaseType' => '960379', + 'Name' => 'GSettingsPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960667' => { + 'BaseType' => '960679', + 'Header' => 'gtktypes.h', + 'Line' => '48', + 'Name' => 'GtkWindow', + 'PrivateABI' => 1, + 'Size' => '56', + 'Type' => 'Typedef' + }, + '960679' => { + 'Header' => 'gtkwindow.h', + 'Line' => '53', + 'Memb' => { + '0' => { + 'name' => 'bin', + 'offset' => '0', + 'type' => '960820' + }, + '1' => { + 'name' => 'priv', + 'offset' => '72', + 'type' => '960911' + } + }, + 'Name' => 'struct _GtkWindow', + 'PrivateABI' => 1, + 'Size' => '56', + 'Type' => 'Struct' + }, + '960746' => { + 'BaseType' => '960758', + 'Header' => 'gtkcontainer.h', + 'Line' => '45', + 'Name' => 'GtkContainer', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Typedef' + }, + '960758' => { + 'Header' => 'gtkcontainer.h', + 'Line' => '49', + 'Memb' => { + '0' => { + 'name' => 'widget', + 'offset' => '0', + 'type' => '941062' + }, + '1' => { + 'name' => 'priv', + 'offset' => '50', + 'type' => '960815' + } + }, + 'Name' => 'struct _GtkContainer', + 'PrivateABI' => 1, + 'Size' => '40', + 'Type' => 'Struct' + }, + '960798' => { + 'BaseType' => '960810', + 'Header' => 'gtkcontainer.h', + 'Line' => '46', + 'Name' => 'GtkContainerPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960810' => { + 'Name' => 'struct _GtkContainerPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960815' => { + 'BaseType' => '960798', + 'Name' => 'GtkContainerPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960820' => { + 'BaseType' => '960832', + 'Header' => 'gtkbin.h', + 'Line' => '45', + 'Name' => 'GtkBin', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Typedef' + }, + '960832' => { + 'Header' => 'gtkbin.h', + 'Line' => '49', + 'Memb' => { + '0' => { + 'name' => 'container', + 'offset' => '0', + 'type' => '960746' + }, + '1' => { + 'name' => 'priv', + 'offset' => '64', + 'type' => '960889' + } + }, + 'Name' => 'struct _GtkBin', + 'PrivateABI' => 1, + 'Size' => '48', + 'Type' => 'Struct' + }, + '960872' => { + 'BaseType' => '960884', + 'Header' => 'gtkbin.h', + 'Line' => '46', + 'Name' => 'GtkBinPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960884' => { + 'Name' => 'struct _GtkBinPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960889' => { + 'BaseType' => '960872', + 'Name' => 'GtkBinPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960894' => { + 'BaseType' => '960906', + 'Header' => 'gtkwindow.h', + 'Line' => '46', + 'Name' => 'GtkWindowPrivate', + 'PrivateABI' => 1, + 'Type' => 'Typedef' + }, + '960906' => { + 'Name' => 'struct _GtkWindowPrivate', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '960911' => { + 'BaseType' => '960894', + 'Name' => 'GtkWindowPrivate*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960916' => { + 'BaseType' => '960928', + 'Header' => 'layersurface.h', + 'Line' => '15', + 'Name' => 'PhoshLayerSurface', + 'Size' => '56', + 'Type' => 'Typedef' + }, + '960928' => { + 'Header' => 'layersurface.h', + 'Line' => '15', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '960667' + } + }, + 'Name' => 'struct _PhoshLayerSurface', + 'Size' => '56', + 'Type' => 'Struct' + }, + '960960' => { + 'BaseType' => '960965', + 'Name' => 'struct wl_output*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '960965' => { + 'Name' => 'struct wl_output', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '961011' => { + 'BaseType' => '961016', + 'Name' => 'struct zwlr_gamma_control_v1*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '961016' => { + 'Name' => 'struct zwlr_gamma_control_v1', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '961021' => { + 'BaseType' => '961026', + 'Name' => 'struct zwlr_output_power_v1*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '961026' => { + 'Name' => 'struct zwlr_output_power_v1', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '961031' => { + 'BaseType' => '961036', + 'Name' => 'struct zxdg_output_v1*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '961036' => { + 'Name' => 'struct zxdg_output_v1', + 'PrivateABI' => 1, + 'Type' => 'Struct' + }, + '961063' => { + 'BaseType' => '961075', + 'Header' => 'backlight.h', + 'Line' => '21', + 'Name' => 'PhoshBacklight', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '961075' => { + 'Header' => 'backlight.h', + 'Line' => '21', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + } + }, + 'Name' => 'struct _PhoshBacklight', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '961102' => { + 'BaseType' => '961063', + 'Name' => 'PhoshBacklight*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '961107' => { + 'Header' => 'monitor.h', + 'Line' => '42', + 'Memb' => { + '0' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_Unknown', + 'value' => '0' + }, + '1' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_VGA', + 'value' => '1' + }, + '10' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_DisplayPort', + 'value' => '10' + }, + '11' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_HDMIA', + 'value' => '11' + }, + '12' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_HDMIB', + 'value' => '12' + }, + '13' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_TV', + 'value' => '13' + }, + '14' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_eDP', + 'value' => '14' + }, + '15' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL', + 'value' => '15' + }, + '16' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_DSI', + 'value' => '16' + }, + '2' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_DVII', + 'value' => '2' + }, + '3' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_DVID', + 'value' => '3' + }, + '4' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_DVIA', + 'value' => '4' + }, + '5' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_Composite', + 'value' => '5' + }, + '6' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_SVIDEO', + 'value' => '6' + }, + '7' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_LVDS', + 'value' => '7' + }, + '8' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_Component', + 'value' => '8' + }, + '9' => { + 'name' => 'PHOSH_MONITOR_CONNECTOR_TYPE_9PinDIN', + 'value' => '9' + } + }, + 'Name' => 'enum _PhoshMonitorConnectorType', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '961226' => { + 'BaseType' => '961107', + 'Header' => 'monitor.h', + 'Line' => '61', + 'Name' => 'PhoshMonitorConnectorType', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '961238' => { + 'Header' => 'monitor.h', + 'Line' => '104', + 'Memb' => { + '0' => { + 'name' => 'PHOSH_MONITOR_POWER_SAVE_MODE_OFF', + 'value' => '0' + }, + '1' => { + 'name' => 'PHOSH_MONITOR_POWER_SAVE_MODE_ON', + 'value' => '1' + } + }, + 'Name' => 'enum _PhoshMonitorPowerSaveMode', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '961267' => { + 'BaseType' => '961238', + 'Header' => 'monitor.h', + 'Line' => '107', + 'Name' => 'PhoshMonitorPowerSaveMode', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Typedef' + }, + '961279' => { + 'Header' => 'monitor.h', + 'Line' => '123', + 'Memb' => { + '0' => { + 'name' => 'x', + 'offset' => '0', + 'type' => '693750' + }, + '1' => { + 'name' => 'y', + 'offset' => '4', + 'type' => '693750' + }, + '2' => { + 'name' => 'width', + 'offset' => '8', + 'type' => '693750' + }, + '3' => { + 'name' => 'height', + 'offset' => '18', + 'type' => '693750' + } + }, + 'Name' => 'struct PhoshLogicalSize', + 'PrivateABI' => 1, + 'Size' => '16', + 'Type' => 'Struct' + }, + '961341' => { + 'Header' => 'monitor.h', + 'Line' => '111', + 'Memb' => { + '0' => { + 'name' => 'parent', + 'offset' => '0', + 'type' => '3321' + }, + '1' => { + 'name' => 'wl_output', + 'offset' => '36', + 'type' => '960960' + }, + '10' => { + 'name' => 'transform', + 'offset' => '114', + 'type' => '693750' + }, + '11' => { + 'name' => 'logical', + 'offset' => '118', + 'type' => '961279' + }, + '12' => { + 'name' => 'width_mm', + 'offset' => '146', + 'type' => '60' + }, + '13' => { + 'name' => 'height_mm', + 'offset' => '150', + 'type' => '60' + }, + '14' => { + 'name' => 'description', + 'offset' => '260', + 'type' => '190' + }, + '15' => { + 'name' => 'modes', + 'offset' => '274', + 'type' => '933212' + }, + '16' => { + 'name' => 'current_mode', + 'offset' => '288', + 'type' => '277' + }, + '17' => { + 'name' => 'preferred_mode', + 'offset' => '292', + 'type' => '277' + }, + '18' => { + 'name' => 'name', + 'offset' => '296', + 'type' => '190' + }, + '19' => { + 'name' => 'conn_type', + 'offset' => '310', + 'type' => '961226' + }, + '2' => { + 'name' => 'xdg_output', + 'offset' => '50', + 'type' => '961031' + }, + '20' => { + 'name' => 'wl_output_done', + 'offset' => '320', + 'type' => '253' + }, + '21' => { + 'name' => 'gamma_control', + 'offset' => '324', + 'type' => '961011' + }, + '22' => { + 'name' => 'n_gamma_entries', + 'offset' => '338', + 'type' => '121' + }, + '23' => { + 'name' => 'backlight', + 'offset' => '352', + 'type' => '961102' + }, + '3' => { + 'name' => 'wlr_output_power', + 'offset' => '64', + 'type' => '961021' + }, + '4' => { + 'name' => 'power_mode', + 'offset' => '72', + 'type' => '961267' + }, + '5' => { + 'name' => 'x', + 'offset' => '82', + 'type' => '60' + }, + '6' => { + 'name' => 'y', + 'offset' => '86', + 'type' => '60' + }, + '7' => { + 'name' => 'width', + 'offset' => '96', + 'type' => '60' + }, + '8' => { + 'name' => 'height', + 'offset' => '100', + 'type' => '60' + }, + '9' => { + 'name' => 'subpixel', + 'offset' => '104', + 'type' => '60' + } + }, + 'Name' => 'struct _PhoshMonitor', + 'PrivateABI' => 1, + 'Size' => '168', + 'Type' => 'Struct' + }, + '961663' => { + 'BaseType' => '961341', + 'Header' => 'monitor.h', + 'Line' => '147', + 'Name' => 'PhoshMonitor', + 'PrivateABI' => 1, + 'Size' => '168', + 'Type' => 'Typedef' + }, + '961675' => { + 'BaseType' => '961663', + 'Name' => 'PhoshMonitor*', + 'Size' => '8', + 'Type' => 'Pointer' + }, + '961736' => { + 'Header' => 'gdesktop-enums.h', + 'Line' => '56', + 'Memb' => { + '0' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_NONE', + 'value' => '0' + }, + '1' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_WALLPAPER', + 'value' => '1' + }, + '2' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_CENTERED', + 'value' => '2' + }, + '3' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_SCALED', + 'value' => '3' + }, + '4' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_STRETCHED', + 'value' => '4' + }, + '5' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_ZOOM', + 'value' => '5' + }, + '6' => { + 'name' => 'G_DESKTOP_BACKGROUND_STYLE_SPANNED', + 'value' => '6' + } + }, + 'Name' => 'enum GDesktopBackgroundStyle', + 'PrivateABI' => 1, + 'Size' => '4', + 'Type' => 'Enum' + }, + '961897' => { + 'BaseType' => '961909', + 'Header' => 'manager.h', + 'Line' => '15', + 'Name' => 'PhoshManager', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Typedef' + }, + '961909' => { + 'Header' => 'manager.h', + 'Line' => '15', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '3321' + } + }, + 'Name' => 'struct _PhoshManager', + 'PrivateABI' => 1, + 'Size' => '24', + 'Type' => 'Struct' + }, + '991525' => { + 'BaseType' => '991537', + 'Header' => 'status-icon.h', + 'Line' => '15', + 'Name' => 'PhoshStatusIcon', + 'Size' => '48', + 'Type' => 'Typedef' + }, + '991537' => { + 'Header' => 'status-icon.h', + 'Line' => '15', + 'Memb' => { + '0' => { + 'name' => 'parent_instance', + 'offset' => '0', + 'type' => '960820' + } + }, + 'Name' => 'struct _PhoshStatusIcon', + 'Size' => '48', + 'Type' => 'Struct' + }, + '991743' => { + 'BaseType' => '991525', + 'Name' => 'PhoshStatusIcon*', + 'Size' => '8', + 'Type' => 'Pointer' + } + }, + 'UndefinedSymbols' => { + 'libphosh-0.45.so.0' => { + '_ITM_deregisterTMCloneTable' => 0, + '_ITM_registerTMCloneTable' => 0, + '__cxa_finalize@GLIBC_2.2.5' => 0, + '__errno_location@GLIBC_2.2.5' => 0, + '__gmon_start__' => 0, + '__stack_chk_fail@GLIBC_2.4' => 0, + 'as_component_box_index_safe' => 0, + 'as_component_box_is_empty' => 0, + 'as_component_get_data_id' => 0, + 'as_pool_get_components_by_launchable' => 0, + 'as_pool_load_async' => 0, + 'as_pool_load_finish' => 0, + 'as_pool_new' => 0, + 'as_pool_set_flags' => 0, + 'bind_textdomain_codeset@GLIBC_2.2.5' => 0, + 'bindtextdomain@GLIBC_2.2.5' => 0, + 'bluetooth_adapter_state_get_type' => 0, + 'bluetooth_client_connect_service' => 0, + 'bluetooth_client_connect_service_finish' => 0, + 'bluetooth_client_get_devices' => 0, + 'bluetooth_client_get_type' => 0, + 'bluetooth_client_new' => 0, + 'bluetooth_device_get_object_path' => 0, + 'bluetooth_device_get_type' => 0, + 'cairo_arc' => 0, + 'cairo_clip' => 0, + 'cairo_close_path' => 0, + 'cairo_create' => 0, + 'cairo_destroy' => 0, + 'cairo_fill' => 0, + 'cairo_image_surface_create' => 0, + 'cairo_image_surface_create_for_data' => 0, + 'cairo_image_surface_get_height' => 0, + 'cairo_image_surface_get_width' => 0, + 'cairo_line_to' => 0, + 'cairo_move_to' => 0, + 'cairo_new_path' => 0, + 'cairo_paint' => 0, + 'cairo_pattern_add_color_stop_rgba' => 0, + 'cairo_pattern_create_linear' => 0, + 'cairo_pattern_destroy' => 0, + 'cairo_pop_group_to_source' => 0, + 'cairo_push_group' => 0, + 'cairo_rectangle' => 0, + 'cairo_region_create_rectangle' => 0, + 'cairo_region_destroy' => 0, + 'cairo_restore' => 0, + 'cairo_save' => 0, + 'cairo_scale' => 0, + 'cairo_set_line_cap' => 0, + 'cairo_set_line_width' => 0, + 'cairo_set_operator' => 0, + 'cairo_set_source' => 0, + 'cairo_set_source_rgb' => 0, + 'cairo_set_source_rgba' => 0, + 'cairo_set_source_surface' => 0, + 'cairo_stroke' => 0, + 'cairo_surface_destroy' => 0, + 'cairo_translate' => 0, + 'call_audio_deinit@LIBCALLAUDIO_0_0_0' => 0, + 'call_audio_enable_speaker_async@LIBCALLAUDIO_0_0_0' => 0, + 'call_audio_init@LIBCALLAUDIO_0_0_0' => 0, + 'call_audio_is_inited@LIBCALLAUDIO_0_0_0' => 0, + 'call_audio_mute_mic_async@LIBCALLAUDIO_0_0_0' => 0, + 'call_audio_select_mode_async@LIBCALLAUDIO_0_0_0' => 0, + 'clock_gettime@GLIBC_2.17' => 0, + 'close@GLIBC_2.2.5' => 0, + 'cos@GLIBC_2.2.5' => 0, + 'dcgettext@GLIBC_2.2.5' => 0, + 'dcngettext@GLIBC_2.2.5' => 0, + 'exp10@GLIBC_2.39' => 0, + 'fcntl64@GLIBC_2.28' => 0, + 'fmaxf@GLIBC_2.2.5' => 0, + 'fmodf@GLIBC_2.38' => 0, + 'fribidi_get_bidi_type' => 0, + 'ftruncate64@GLIBC_2.2.5' => 0, + 'g_action_get_enabled' => 0, + 'g_action_group_activate_action' => 0, + 'g_action_group_change_action_state' => 0, + 'g_action_group_get_action_parameter_type' => 0, + 'g_action_group_get_type' => 0, + 'g_action_group_has_action' => 0, + 'g_action_group_list_actions' => 0, + 'g_action_group_query_action' => 0, + 'g_action_map_add_action' => 0, + 'g_action_map_add_action_entries' => 0, + 'g_action_map_get_type' => 0, + 'g_action_map_lookup_action' => 0, + 'g_action_map_remove_action' => 0, + 'g_app_info_equal' => 0, + 'g_app_info_get_all' => 0, + 'g_app_info_get_default_for_type' => 0, + 'g_app_info_get_description' => 0, + 'g_app_info_get_display_name' => 0, + 'g_app_info_get_executable' => 0, + 'g_app_info_get_icon' => 0, + 'g_app_info_get_id' => 0, + 'g_app_info_get_name' => 0, + 'g_app_info_get_type' => 0, + 'g_app_info_launch_uris_async' => 0, + 'g_app_info_launch_uris_finish' => 0, + 'g_app_info_monitor_get' => 0, + 'g_app_info_should_show' => 0, + 'g_array_append_vals' => 0, + 'g_array_free' => 0, + 'g_array_new' => 0, + 'g_array_set_size' => 0, + 'g_array_unref' => 0, + 'g_ascii_strcasecmp' => 0, + 'g_ascii_strdown' => 0, + 'g_ascii_strtoll' => 0, + 'g_ascii_table' => 0, + 'g_ascii_tolower' => 0, + 'g_ask_password_flags_get_type' => 0, + 'g_assertion_message' => 0, + 'g_assertion_message_expr' => 0, + 'g_async_initable_get_type' => 0, + 'g_async_initable_new_async' => 0, + 'g_async_initable_new_finish' => 0, + 'g_async_result_get_source_object' => 0, + 'g_async_result_is_tagged' => 0, + 'g_atomic_ref_count_dec' => 0, + 'g_atomic_ref_count_inc' => 0, + 'g_atomic_ref_count_init' => 0, + 'g_binding_unbind' => 0, + 'g_bookmark_file_add_application' => 0, + 'g_bookmark_file_free' => 0, + 'g_bookmark_file_load_from_file' => 0, + 'g_bookmark_file_new' => 0, + 'g_bookmark_file_to_file' => 0, + 'g_boxed_type_register_static' => 0, + 'g_build_filename' => 0, + 'g_build_path' => 0, + 'g_bus_get' => 0, + 'g_bus_get_finish' => 0, + 'g_bus_own_name' => 0, + 'g_bus_unown_name' => 0, + 'g_bus_unwatch_name' => 0, + 'g_bus_watch_name' => 0, + 'g_bus_watch_name_on_connection' => 0, + 'g_bytes_get_data' => 0, + 'g_bytes_get_size' => 0, + 'g_bytes_new' => 0, + 'g_bytes_new_take' => 0, + 'g_bytes_unref' => 0, + 'g_cancellable_cancel' => 0, + 'g_cancellable_connect' => 0, + 'g_cancellable_disconnect' => 0, + 'g_cancellable_get_type' => 0, + 'g_cancellable_new' => 0, + 'g_cclosure_marshal_VOID__BOOLEAN' => 0, + 'g_cclosure_marshal_VOID__OBJECT' => 0, + 'g_cclosure_marshal_VOID__STRING' => 0, + 'g_cclosure_marshal_VOID__UINT' => 0, + 'g_cclosure_marshal_VOID__VOID' => 0, + 'g_checksum_free' => 0, + 'g_checksum_get_digest' => 0, + 'g_checksum_get_string' => 0, + 'g_checksum_new' => 0, + 'g_checksum_update' => 0, + 'g_child_watch_add' => 0, + 'g_clear_error' => 0, + 'g_close' => 0, + 'g_datalist_clear' => 0, + 'g_datalist_id_set_data_full' => 0, + 'g_date_time_add_days' => 0, + 'g_date_time_add_hours' => 0, + 'g_date_time_add_minutes' => 0, + 'g_date_time_add_months' => 0, + 'g_date_time_add_seconds' => 0, + 'g_date_time_compare' => 0, + 'g_date_time_difference' => 0, + 'g_date_time_format' => 0, + 'g_date_time_get_type' => 0, + 'g_date_time_new_now_local' => 0, + 'g_date_time_ref' => 0, + 'g_date_time_to_unix' => 0, + 'g_date_time_unref' => 0, + 'g_dbus_connection_call' => 0, + 'g_dbus_connection_call_finish' => 0, + 'g_dbus_connection_emit_signal' => 0, + 'g_dbus_connection_get_type' => 0, + 'g_dbus_connection_signal_subscribe' => 0, + 'g_dbus_connection_signal_unsubscribe' => 0, + 'g_dbus_error_quark' => 0, + 'g_dbus_error_strip_remote_error' => 0, + 'g_dbus_gvalue_to_gvariant' => 0, + 'g_dbus_gvariant_to_gvalue' => 0, + 'g_dbus_interface_get_info' => 0, + 'g_dbus_interface_info_lookup_property' => 0, + 'g_dbus_interface_info_lookup_signal' => 0, + 'g_dbus_interface_skeleton_export' => 0, + 'g_dbus_interface_skeleton_get_connection' => 0, + 'g_dbus_interface_skeleton_get_connections' => 0, + 'g_dbus_interface_skeleton_get_object_path' => 0, + 'g_dbus_interface_skeleton_get_type' => 0, + 'g_dbus_interface_skeleton_unexport' => 0, + 'g_dbus_message_get_unix_fd_list' => 0, + 'g_dbus_method_invocation_get_connection' => 0, + 'g_dbus_method_invocation_get_message' => 0, + 'g_dbus_method_invocation_get_method_info' => 0, + 'g_dbus_method_invocation_get_sender' => 0, + 'g_dbus_method_invocation_get_type' => 0, + 'g_dbus_method_invocation_return_error' => 0, + 'g_dbus_method_invocation_return_value' => 0, + 'g_dbus_method_invocation_return_value_with_unix_fd_list' => 0, + 'g_dbus_object_get_interface' => 0, + 'g_dbus_object_get_object_path' => 0, + 'g_dbus_object_get_type' => 0, + 'g_dbus_object_manager_client_get_name' => 0, + 'g_dbus_object_manager_client_get_name_owner' => 0, + 'g_dbus_object_manager_client_get_type' => 0, + 'g_dbus_object_manager_get_object_path' => 0, + 'g_dbus_object_manager_get_objects' => 0, + 'g_dbus_object_manager_server_export' => 0, + 'g_dbus_object_manager_server_new' => 0, + 'g_dbus_object_manager_server_set_connection' => 0, + 'g_dbus_object_proxy_get_type' => 0, + 'g_dbus_object_skeleton_add_interface' => 0, + 'g_dbus_object_skeleton_get_type' => 0, + 'g_dbus_object_skeleton_remove_interface_by_name' => 0, + 'g_dbus_proxy_call' => 0, + 'g_dbus_proxy_call_finish' => 0, + 'g_dbus_proxy_call_sync' => 0, + 'g_dbus_proxy_call_with_unix_fd_list' => 0, + 'g_dbus_proxy_call_with_unix_fd_list_finish' => 0, + 'g_dbus_proxy_call_with_unix_fd_list_sync' => 0, + 'g_dbus_proxy_get_cached_property' => 0, + 'g_dbus_proxy_get_name' => 0, + 'g_dbus_proxy_get_name_owner' => 0, + 'g_dbus_proxy_get_object_path' => 0, + 'g_dbus_proxy_get_type' => 0, + 'g_dbus_proxy_new_for_bus' => 0, + 'g_dbus_proxy_new_for_bus_finish' => 0, + 'g_dbus_proxy_set_interface_info' => 0, + 'g_desktop_app_info_get_action_name' => 0, + 'g_desktop_app_info_get_boolean' => 0, + 'g_desktop_app_info_get_categories' => 0, + 'g_desktop_app_info_get_generic_name' => 0, + 'g_desktop_app_info_get_keywords' => 0, + 'g_desktop_app_info_get_startup_wm_class' => 0, + 'g_desktop_app_info_get_string' => 0, + 'g_desktop_app_info_get_string_list' => 0, + 'g_desktop_app_info_get_type' => 0, + 'g_desktop_app_info_launch_action' => 0, + 'g_desktop_app_info_launch_uris_as_manager' => 0, + 'g_desktop_app_info_list_actions' => 0, + 'g_desktop_app_info_new' => 0, + 'g_dgettext' => 0, + 'g_direct_equal' => 0, + 'g_direct_hash' => 0, + 'g_dpgettext' => 0, + 'g_drive_get_name' => 0, + 'g_drive_get_type' => 0, + 'g_enum_register_static' => 0, + 'g_enum_to_string' => 0, + 'g_error_free' => 0, + 'g_error_get_type' => 0, + 'g_error_matches' => 0, + 'g_error_new' => 0, + 'g_file_create' => 0, + 'g_file_equal' => 0, + 'g_file_get_contents' => 0, + 'g_file_get_path' => 0, + 'g_file_get_type' => 0, + 'g_file_get_uri' => 0, + 'g_file_hash' => 0, + 'g_file_icon_new' => 0, + 'g_file_make_directory_with_parents' => 0, + 'g_file_monitor_file' => 0, + 'g_file_new_for_path' => 0, + 'g_file_new_for_uri' => 0, + 'g_file_peek_path' => 0, + 'g_file_read' => 0, + 'g_file_test' => 0, + 'g_filename_to_uri' => 0, + 'g_find_program_in_path' => 0, + 'g_flags_register_static' => 0, + 'g_flags_to_string' => 0, + 'g_free' => 0, + 'g_get_home_dir' => 0, + 'g_get_host_name' => 0, + 'g_get_monotonic_time' => 0, + 'g_get_real_name' => 0, + 'g_get_system_data_dirs' => 0, + 'g_get_user_cache_dir' => 0, + 'g_get_user_data_dir' => 0, + 'g_get_user_name' => 0, + 'g_get_user_special_dir' => 0, + 'g_getenv' => 0, + 'g_hash_table_contains' => 0, + 'g_hash_table_destroy' => 0, + 'g_hash_table_find' => 0, + 'g_hash_table_foreach' => 0, + 'g_hash_table_get_type' => 0, + 'g_hash_table_get_values' => 0, + 'g_hash_table_insert' => 0, + 'g_hash_table_iter_init' => 0, + 'g_hash_table_iter_next' => 0, + 'g_hash_table_iter_remove' => 0, + 'g_hash_table_lookup' => 0, + 'g_hash_table_new' => 0, + 'g_hash_table_new_full' => 0, + 'g_hash_table_remove' => 0, + 'g_hash_table_remove_all' => 0, + 'g_hash_table_replace' => 0, + 'g_hash_table_size' => 0, + 'g_hash_table_unref' => 0, + 'g_hostname_to_ascii' => 0, + 'g_icon_get_type' => 0, + 'g_idle_add' => 0, + 'g_idle_add_once' => 0, + 'g_idle_source_new' => 0, + 'g_initable_get_type' => 0, + 'g_initable_new' => 0, + 'g_input_stream_close' => 0, + 'g_input_stream_read_async' => 0, + 'g_input_stream_read_finish' => 0, + 'g_int_equal' => 0, + 'g_int_hash' => 0, + 'g_intern_static_string' => 0, + 'g_io_add_watch' => 0, + 'g_io_channel_read_chars' => 0, + 'g_io_channel_set_buffered' => 0, + 'g_io_channel_set_encoding' => 0, + 'g_io_channel_shutdown' => 0, + 'g_io_channel_unix_new' => 0, + 'g_io_channel_unref' => 0, + 'g_io_error_from_errno' => 0, + 'g_io_error_quark' => 0, + 'g_io_extension_get_type' => 0, + 'g_io_extension_point_get_extension_by_name' => 0, + 'g_io_extension_point_lookup' => 0, + 'g_io_extension_point_register' => 0, + 'g_io_extension_point_set_required_type' => 0, + 'g_io_modules_scan_all_in_directory' => 0, + 'g_key_file_get_boolean' => 0, + 'g_key_file_get_groups' => 0, + 'g_key_file_get_locale_string' => 0, + 'g_key_file_get_string' => 0, + 'g_key_file_load_from_data' => 0, + 'g_key_file_load_from_file' => 0, + 'g_key_file_new' => 0, + 'g_key_file_unref' => 0, + 'g_list_append' => 0, + 'g_list_concat' => 0, + 'g_list_copy' => 0, + 'g_list_delete_link' => 0, + 'g_list_foreach' => 0, + 'g_list_free' => 0, + 'g_list_free_full' => 0, + 'g_list_last' => 0, + 'g_list_length' => 0, + 'g_list_model_get_item' => 0, + 'g_list_model_get_item_type' => 0, + 'g_list_model_get_n_items' => 0, + 'g_list_model_get_object' => 0, + 'g_list_model_get_type' => 0, + 'g_list_model_items_changed' => 0, + 'g_list_nth_data' => 0, + 'g_list_prepend' => 0, + 'g_list_remove' => 0, + 'g_list_reverse' => 0, + 'g_list_sort' => 0, + 'g_list_store_append' => 0, + 'g_list_store_find' => 0, + 'g_list_store_find_with_equal_func' => 0, + 'g_list_store_get_type' => 0, + 'g_list_store_insert' => 0, + 'g_list_store_new' => 0, + 'g_list_store_remove' => 0, + 'g_list_store_remove_all' => 0, + 'g_loadable_icon_get_type' => 0, + 'g_loadable_icon_load_async' => 0, + 'g_loadable_icon_load_finish' => 0, + 'g_locale_to_utf8' => 0, + 'g_log' => 0, + 'g_log_structured_standard' => 0, + 'g_main_context_default' => 0, + 'g_main_context_ref_thread_default' => 0, + 'g_main_context_unref' => 0, + 'g_malloc' => 0, + 'g_malloc0' => 0, + 'g_malloc0_n' => 0, + 'g_malloc_n' => 0, + 'g_markup_escape_text' => 0, + 'g_match_info_fetch' => 0, + 'g_match_info_unref' => 0, + 'g_memdup2' => 0, + 'g_menu_append' => 0, + 'g_menu_append_item' => 0, + 'g_menu_append_section' => 0, + 'g_menu_item_new_submenu' => 0, + 'g_menu_new' => 0, + 'g_menu_remove_all' => 0, + 'g_mkdir_with_parents' => 0, + 'g_module_supported' => 0, + 'g_mount_get_name' => 0, + 'g_mount_get_root' => 0, + 'g_mount_get_symbolic_icon' => 0, + 'g_mount_get_type' => 0, + 'g_mount_operation_get_type' => 0, + 'g_mount_operation_reply' => 0, + 'g_mount_operation_set_password' => 0, + 'g_mutex_clear' => 0, + 'g_mutex_init' => 0, + 'g_mutex_lock' => 0, + 'g_mutex_unlock' => 0, + 'g_object_add_weak_pointer' => 0, + 'g_object_bind_property' => 0, + 'g_object_bind_property_full' => 0, + 'g_object_class_find_property' => 0, + 'g_object_class_install_properties' => 0, + 'g_object_class_install_property' => 0, + 'g_object_class_override_property' => 0, + 'g_object_connect' => 0, + 'g_object_freeze_notify' => 0, + 'g_object_get' => 0, + 'g_object_get_data' => 0, + 'g_object_get_property' => 0, + 'g_object_interface_install_property' => 0, + 'g_object_new' => 0, + 'g_object_notify' => 0, + 'g_object_notify_by_pspec' => 0, + 'g_object_ref' => 0, + 'g_object_ref_sink' => 0, + 'g_object_remove_weak_pointer' => 0, + 'g_object_run_dispose' => 0, + 'g_object_set' => 0, + 'g_object_set_data' => 0, + 'g_object_set_data_full' => 0, + 'g_object_set_property' => 0, + 'g_object_thaw_notify' => 0, + 'g_object_unref' => 0, + 'g_object_weak_ref' => 0, + 'g_object_weak_unref' => 0, + 'g_once_init_enter' => 0, + 'g_once_init_enter_pointer' => 0, + 'g_once_init_leave' => 0, + 'g_once_init_leave_pointer' => 0, + 'g_output_stream_close' => 0, + 'g_output_stream_write_async' => 0, + 'g_output_stream_write_finish' => 0, + 'g_param_spec_boolean' => 0, + 'g_param_spec_boxed' => 0, + 'g_param_spec_double' => 0, + 'g_param_spec_enum' => 0, + 'g_param_spec_flags' => 0, + 'g_param_spec_float' => 0, + 'g_param_spec_get_name' => 0, + 'g_param_spec_gtype' => 0, + 'g_param_spec_int' => 0, + 'g_param_spec_long' => 0, + 'g_param_spec_object' => 0, + 'g_param_spec_pointer' => 0, + 'g_param_spec_string' => 0, + 'g_param_spec_uint' => 0, + 'g_param_spec_ulong' => 0, + 'g_param_spec_variant' => 0, + 'g_parse_debug_string' => 0, + 'g_path_get_basename' => 0, + 'g_path_get_dirname' => 0, + 'g_path_is_absolute' => 0, + 'g_propagate_error' => 0, + 'g_property_action_new' => 0, + 'g_ptr_array_add' => 0, + 'g_ptr_array_copy' => 0, + 'g_ptr_array_find' => 0, + 'g_ptr_array_free' => 0, + 'g_ptr_array_new' => 0, + 'g_ptr_array_new_with_free_func' => 0, + 'g_ptr_array_ref' => 0, + 'g_ptr_array_remove' => 0, + 'g_ptr_array_remove_fast' => 0, + 'g_ptr_array_remove_range' => 0, + 'g_ptr_array_sized_new' => 0, + 'g_ptr_array_unref' => 0, + 'g_quark_from_static_string' => 0, + 'g_quark_to_string' => 0, + 'g_quark_try_string' => 0, + 'g_random_int_range' => 0, + 'g_regex_match' => 0, + 'g_regex_new' => 0, + 'g_regex_replace_literal' => 0, + 'g_regex_unref' => 0, + 'g_return_if_fail_warning' => 0, + 'g_sequence_append' => 0, + 'g_sequence_free' => 0, + 'g_sequence_get' => 0, + 'g_sequence_get_begin_iter' => 0, + 'g_sequence_get_end_iter' => 0, + 'g_sequence_get_iter_at_pos' => 0, + 'g_sequence_get_length' => 0, + 'g_sequence_insert_before' => 0, + 'g_sequence_insert_sorted' => 0, + 'g_sequence_iter_get_position' => 0, + 'g_sequence_iter_is_begin' => 0, + 'g_sequence_iter_is_end' => 0, + 'g_sequence_iter_next' => 0, + 'g_sequence_iter_prev' => 0, + 'g_sequence_move' => 0, + 'g_sequence_new' => 0, + 'g_sequence_prepend' => 0, + 'g_sequence_remove' => 0, + 'g_sequence_remove_range' => 0, + 'g_sequence_sort' => 0, + 'g_set_error' => 0, + 'g_set_error_literal' => 0, + 'g_settings_bind' => 0, + 'g_settings_get_boolean' => 0, + 'g_settings_get_enum' => 0, + 'g_settings_get_flags' => 0, + 'g_settings_get_int' => 0, + 'g_settings_get_string' => 0, + 'g_settings_get_strv' => 0, + 'g_settings_get_type' => 0, + 'g_settings_get_uint' => 0, + 'g_settings_get_value' => 0, + 'g_settings_new' => 0, + 'g_settings_new_with_path' => 0, + 'g_settings_reset' => 0, + 'g_settings_schema_has_key' => 0, + 'g_settings_schema_source_get_default' => 0, + 'g_settings_schema_source_lookup' => 0, + 'g_settings_schema_unref' => 0, + 'g_settings_set_boolean' => 0, + 'g_settings_set_string' => 0, + 'g_settings_set_strv' => 0, + 'g_shell_parse_argv' => 0, + 'g_signal_accumulator_true_handled' => 0, + 'g_signal_add_emission_hook' => 0, + 'g_signal_connect_data' => 0, + 'g_signal_connect_object' => 0, + 'g_signal_emit' => 0, + 'g_signal_emit_by_name' => 0, + 'g_signal_emitv' => 0, + 'g_signal_handler_block' => 0, + 'g_signal_handler_disconnect' => 0, + 'g_signal_handler_unblock' => 0, + 'g_signal_handlers_block_matched' => 0, + 'g_signal_handlers_disconnect_matched' => 0, + 'g_signal_handlers_unblock_matched' => 0, + 'g_signal_lookup' => 0, + 'g_signal_new' => 0, + 'g_signal_new_class_handler' => 0, + 'g_signal_remove_emission_hook' => 0, + 'g_signal_set_va_marshaller' => 0, + 'g_signal_stop_emission_by_name' => 0, + 'g_simple_action_group_get_type' => 0, + 'g_simple_action_group_new' => 0, + 'g_simple_action_set_enabled' => 0, + 'g_slice_alloc' => 0, + 'g_slice_alloc0' => 0, + 'g_slice_free1' => 0, + 'g_slist_prepend' => 0, + 'g_slist_sort' => 0, + 'g_source_attach' => 0, + 'g_source_destroy' => 0, + 'g_source_remove' => 0, + 'g_source_set_callback' => 0, + 'g_source_set_name' => 0, + 'g_source_set_name_by_id' => 0, + 'g_source_set_priority' => 0, + 'g_source_unref' => 0, + 'g_spawn_async' => 0, + 'g_spawn_async_with_pipes' => 0, + 'g_spawn_check_wait_status' => 0, + 'g_spawn_close_pid' => 0, + 'g_static_resource_fini' => 0, + 'g_static_resource_get_resource' => 0, + 'g_static_resource_init' => 0, + 'g_str_equal' => 0, + 'g_str_has_prefix' => 0, + 'g_str_has_suffix' => 0, + 'g_str_hash' => 0, + 'g_strcanon' => 0, + 'g_strchomp' => 0, + 'g_strchug' => 0, + 'g_strcmp0' => 0, + 'g_strconcat' => 0, + 'g_strdup' => 0, + 'g_strdup_printf' => 0, + 'g_strdup_vprintf' => 0, + 'g_strdupv' => 0, + 'g_strerror' => 0, + 'g_strfreev' => 0, + 'g_string_append_len' => 0, + 'g_string_append_printf' => 0, + 'g_string_free' => 0, + 'g_string_free_and_steal' => 0, + 'g_string_insert_c' => 0, + 'g_string_insert_len' => 0, + 'g_string_new' => 0, + 'g_string_new_len' => 0, + 'g_strjoin' => 0, + 'g_strjoinv' => 0, + 'g_strndup' => 0, + 'g_strrstr' => 0, + 'g_strsplit' => 0, + 'g_strv_builder_add' => 0, + 'g_strv_builder_addv' => 0, + 'g_strv_builder_end' => 0, + 'g_strv_builder_new' => 0, + 'g_strv_builder_take' => 0, + 'g_strv_builder_unref' => 0, + 'g_strv_contains' => 0, + 'g_strv_get_type' => 0, + 'g_strv_length' => 0, + 'g_task_get_name' => 0, + 'g_task_get_source_object' => 0, + 'g_task_get_task_data' => 0, + 'g_task_get_type' => 0, + 'g_task_is_valid' => 0, + 'g_task_new' => 0, + 'g_task_propagate_boolean' => 0, + 'g_task_propagate_int' => 0, + 'g_task_propagate_pointer' => 0, + 'g_task_return_boolean' => 0, + 'g_task_return_error' => 0, + 'g_task_return_int' => 0, + 'g_task_return_new_error' => 0, + 'g_task_return_pointer' => 0, + 'g_task_run_in_thread' => 0, + 'g_task_set_source_tag' => 0, + 'g_task_set_static_name' => 0, + 'g_task_set_task_data' => 0, + 'g_themed_icon_append_name' => 0, + 'g_themed_icon_get_type' => 0, + 'g_themed_icon_new' => 0, + 'g_themed_icon_new_with_default_fallbacks' => 0, + 'g_timeout_add' => 0, + 'g_timeout_add_once' => 0, + 'g_timeout_add_seconds' => 0, + 'g_timeout_add_seconds_once' => 0, + 'g_timer_destroy' => 0, + 'g_timer_elapsed' => 0, + 'g_timer_new' => 0, + 'g_timer_stop' => 0, + 'g_type_add_instance_private' => 0, + 'g_type_add_interface_static' => 0, + 'g_type_check_instance_is_a' => 0, + 'g_type_check_value_holds' => 0, + 'g_type_class_adjust_private_offset' => 0, + 'g_type_class_peek_parent' => 0, + 'g_type_class_ref' => 0, + 'g_type_class_unref' => 0, + 'g_type_ensure' => 0, + 'g_type_interface_add_prerequisite' => 0, + 'g_type_interface_peek' => 0, + 'g_type_interface_peek_parent' => 0, + 'g_type_is_a' => 0, + 'g_type_name' => 0, + 'g_type_register_static_simple' => 0, + 'g_udev_client_new' => 0, + 'g_udev_client_query_by_subsystem' => 0, + 'g_udev_device_get_name' => 0, + 'g_udev_device_get_parent' => 0, + 'g_udev_device_get_subsystem' => 0, + 'g_udev_device_get_sysfs_attr' => 0, + 'g_udev_device_get_sysfs_attr_as_int' => 0, + 'g_udev_device_get_sysfs_attr_as_int_uncached' => 0, + 'g_udev_device_get_sysfs_path' => 0, + 'g_udev_device_get_type' => 0, + 'g_udev_enumerator_add_match_name' => 0, + 'g_udev_enumerator_add_match_subsystem' => 0, + 'g_udev_enumerator_execute' => 0, + 'g_udev_enumerator_new' => 0, + 'g_unix_fd_list_get' => 0, + 'g_unix_fd_list_get_length' => 0, + 'g_unix_fd_list_get_type' => 0, + 'g_unix_input_stream_new' => 0, + 'g_unix_output_stream_new' => 0, + 'g_unsetenv' => 0, + 'g_uri_peek_scheme' => 0, + 'g_utf8_casefold' => 0, + 'g_utf8_collate' => 0, + 'g_utf8_find_next_char' => 0, + 'g_utf8_get_char' => 0, + 'g_utf8_skip' => 0, + 'g_utf8_strdown' => 0, + 'g_utf8_validate' => 0, + 'g_uuid_string_random' => 0, + 'g_value_copy' => 0, + 'g_value_dup_boxed' => 0, + 'g_value_dup_object' => 0, + 'g_value_dup_string' => 0, + 'g_value_dup_variant' => 0, + 'g_value_get_boolean' => 0, + 'g_value_get_boxed' => 0, + 'g_value_get_double' => 0, + 'g_value_get_enum' => 0, + 'g_value_get_flags' => 0, + 'g_value_get_float' => 0, + 'g_value_get_gtype' => 0, + 'g_value_get_int' => 0, + 'g_value_get_int64' => 0, + 'g_value_get_long' => 0, + 'g_value_get_object' => 0, + 'g_value_get_pointer' => 0, + 'g_value_get_string' => 0, + 'g_value_get_uchar' => 0, + 'g_value_get_uint' => 0, + 'g_value_get_uint64' => 0, + 'g_value_get_ulong' => 0, + 'g_value_get_variant' => 0, + 'g_value_init' => 0, + 'g_value_peek_pointer' => 0, + 'g_value_set_boolean' => 0, + 'g_value_set_boxed' => 0, + 'g_value_set_double' => 0, + 'g_value_set_enum' => 0, + 'g_value_set_flags' => 0, + 'g_value_set_float' => 0, + 'g_value_set_gtype' => 0, + 'g_value_set_int' => 0, + 'g_value_set_long' => 0, + 'g_value_set_object' => 0, + 'g_value_set_pointer' => 0, + 'g_value_set_schar' => 0, + 'g_value_set_string' => 0, + 'g_value_set_uint' => 0, + 'g_value_set_ulong' => 0, + 'g_value_set_variant' => 0, + 'g_value_take_object' => 0, + 'g_value_take_string' => 0, + 'g_value_unset' => 0, + 'g_variant_builder_add' => 0, + 'g_variant_builder_add_value' => 0, + 'g_variant_builder_clear' => 0, + 'g_variant_builder_end' => 0, + 'g_variant_builder_init' => 0, + 'g_variant_builder_init_static' => 0, + 'g_variant_builder_new' => 0, + 'g_variant_builder_unref' => 0, + 'g_variant_dict_clear' => 0, + 'g_variant_dict_end' => 0, + 'g_variant_dict_init' => 0, + 'g_variant_dict_insert' => 0, + 'g_variant_dict_insert_value' => 0, + 'g_variant_dict_lookup' => 0, + 'g_variant_dict_new' => 0, + 'g_variant_dict_unref' => 0, + 'g_variant_dup_string' => 0, + 'g_variant_equal' => 0, + 'g_variant_get' => 0, + 'g_variant_get_boolean' => 0, + 'g_variant_get_byte' => 0, + 'g_variant_get_bytestring' => 0, + 'g_variant_get_child' => 0, + 'g_variant_get_child_value' => 0, + 'g_variant_get_data' => 0, + 'g_variant_get_double' => 0, + 'g_variant_get_int32' => 0, + 'g_variant_get_int64' => 0, + 'g_variant_get_size' => 0, + 'g_variant_get_string' => 0, + 'g_variant_get_type_string' => 0, + 'g_variant_get_uint32' => 0, + 'g_variant_get_variant' => 0, + 'g_variant_is_object_path' => 0, + 'g_variant_is_of_type' => 0, + 'g_variant_iter_free' => 0, + 'g_variant_iter_init' => 0, + 'g_variant_iter_loop' => 0, + 'g_variant_iter_n_children' => 0, + 'g_variant_iter_next' => 0, + 'g_variant_iter_next_value' => 0, + 'g_variant_lookup' => 0, + 'g_variant_lookup_value' => 0, + 'g_variant_n_children' => 0, + 'g_variant_new' => 0, + 'g_variant_new_array' => 0, + 'g_variant_new_boolean' => 0, + 'g_variant_new_from_bytes' => 0, + 'g_variant_new_int32' => 0, + 'g_variant_new_int64' => 0, + 'g_variant_new_parsed' => 0, + 'g_variant_new_string' => 0, + 'g_variant_new_take_string' => 0, + 'g_variant_new_tuple' => 0, + 'g_variant_new_uint32' => 0, + 'g_variant_ref' => 0, + 'g_variant_ref_sink' => 0, + 'g_variant_take_ref' => 0, + 'g_variant_type_checked_' => 0, + 'g_variant_unref' => 0, + 'g_volume_can_mount' => 0, + 'g_volume_get_mount' => 0, + 'g_volume_get_name' => 0, + 'g_volume_get_type' => 0, + 'g_volume_monitor_get' => 0, + 'g_volume_monitor_get_volumes' => 0, + 'g_volume_mount' => 0, + 'g_volume_mount_finish' => 0, + 'g_volume_should_automount' => 0, + 'g_warn_message' => 0, + 'gcr_prompt_close' => 0, + 'gcr_prompt_get_type' => 0, + 'gcr_prompt_set_warning' => 0, + 'gcr_secure_entry_buffer_new' => 0, + 'gcr_system_prompter_get_type' => 0, + 'gcr_system_prompter_new' => 0, + 'gcr_system_prompter_register' => 0, + 'gcr_system_prompter_unregister' => 0, + 'gdk_cairo_set_source_pixbuf' => 0, + 'gdk_device_get_source' => 0, + 'gdk_display_get_app_launch_context' => 0, + 'gdk_display_get_default' => 0, + 'gdk_display_get_default_seat' => 0, + 'gdk_event_get_keyval' => 0, + 'gdk_event_get_source_device' => 0, + 'gdk_event_triggers_context_menu' => 0, + 'gdk_frame_clock_get_frame_time' => 0, + 'gdk_pixbuf_apply_embedded_orientation' => 0, + 'gdk_pixbuf_composite' => 0, + 'gdk_pixbuf_copy_area' => 0, + 'gdk_pixbuf_copy_options' => 0, + 'gdk_pixbuf_fill' => 0, + 'gdk_pixbuf_flip' => 0, + 'gdk_pixbuf_get_from_surface' => 0, + 'gdk_pixbuf_get_has_alpha' => 0, + 'gdk_pixbuf_get_height' => 0, + 'gdk_pixbuf_get_type' => 0, + 'gdk_pixbuf_get_width' => 0, + 'gdk_pixbuf_loader_close' => 0, + 'gdk_pixbuf_loader_get_pixbuf' => 0, + 'gdk_pixbuf_loader_new' => 0, + 'gdk_pixbuf_loader_write_bytes' => 0, + 'gdk_pixbuf_new' => 0, + 'gdk_pixbuf_new_from_bytes' => 0, + 'gdk_pixbuf_new_from_data' => 0, + 'gdk_pixbuf_new_from_stream' => 0, + 'gdk_pixbuf_new_from_stream_async' => 0, + 'gdk_pixbuf_new_from_stream_finish' => 0, + 'gdk_pixbuf_rotate_simple' => 0, + 'gdk_pixbuf_save_to_stream_async' => 0, + 'gdk_pixbuf_save_to_stream_finish' => 0, + 'gdk_pixbuf_scale' => 0, + 'gdk_pixbuf_scale_simple' => 0, + 'gdk_rectangle_get_type' => 0, + 'gdk_rectangle_intersect' => 0, + 'gdk_rgba_equal' => 0, + 'gdk_rgba_parse' => 0, + 'gdk_screen_get_default' => 0, + 'gdk_seat_get_slaves' => 0, + 'gdk_seat_get_type' => 0, + 'gdk_set_allowed_backends' => 0, + 'gdk_wayland_display_get_wl_display' => 0, + 'gdk_wayland_window_get_wl_surface' => 0, + 'gdk_wayland_window_set_use_custom_surface' => 0, + 'getpid@GLIBC_2.2.5' => 0, + 'getpwuid_r@GLIBC_2.2.5' => 0, + 'getuid@GLIBC_2.2.5' => 0, + 'gm_cutout_get_bounds' => 0, + 'gm_device_info_get_display_panel' => 0, + 'gm_device_info_new' => 0, + 'gm_device_tree_get_compatibles' => 0, + 'gm_display_panel_get_border_radius' => 0, + 'gm_display_panel_get_cutouts' => 0, + 'gm_display_panel_get_type' => 0, + 'gnome_bg_slide_show_get_slide' => 0, + 'gnome_bg_slide_show_get_type' => 0, + 'gnome_bg_slide_show_load_async' => 0, + 'gnome_bg_slide_show_new' => 0, + 'gnome_start_systemd_scope' => 0, + 'gnome_start_systemd_scope_finish' => 0, + 'gnome_wall_clock_get_clock' => 0, + 'gnome_wall_clock_get_type' => 0, + 'gnome_wall_clock_new' => 0, + 'gnome_wall_clock_string_for_datetime' => 0, + 'gnome_xkb_info_get_layout_info' => 0, + 'gnome_xkb_info_new' => 0, + 'gtk_adjustment_get_value' => 0, + 'gtk_adjustment_new' => 0, + 'gtk_adjustment_set_page_increment' => 0, + 'gtk_adjustment_set_step_increment' => 0, + 'gtk_adjustment_set_upper' => 0, + 'gtk_adjustment_set_value' => 0, + 'gtk_bin_get_child' => 0, + 'gtk_bin_get_type' => 0, + 'gtk_binding_entry_add_signal' => 0, + 'gtk_binding_set_by_class' => 0, + 'gtk_box_get_type' => 0, + 'gtk_box_pack_end' => 0, + 'gtk_box_pack_start' => 0, + 'gtk_box_reorder_child' => 0, + 'gtk_box_set_center_widget' => 0, + 'gtk_buildable_get_type' => 0, + 'gtk_button_clicked' => 0, + 'gtk_button_get_type' => 0, + 'gtk_button_new_with_label' => 0, + 'gtk_button_set_label' => 0, + 'gtk_clipboard_get_for_display' => 0, + 'gtk_clipboard_set_image' => 0, + 'gtk_container_add' => 0, + 'gtk_container_class_handle_border_width' => 0, + 'gtk_container_foreach' => 0, + 'gtk_container_get_children' => 0, + 'gtk_container_get_type' => 0, + 'gtk_container_propagate_draw' => 0, + 'gtk_container_remove' => 0, + 'gtk_css_provider_load_from_data' => 0, + 'gtk_css_provider_load_from_resource' => 0, + 'gtk_css_provider_new' => 0, + 'gtk_drawing_area_get_type' => 0, + 'gtk_editable_delete_text' => 0, + 'gtk_editable_get_type' => 0, + 'gtk_editable_insert_text' => 0, + 'gtk_entry_buffer_get_text' => 0, + 'gtk_entry_buffer_set_text' => 0, + 'gtk_entry_get_alignment' => 0, + 'gtk_entry_get_text' => 0, + 'gtk_entry_get_text_length' => 0, + 'gtk_entry_get_type' => 0, + 'gtk_entry_grab_focus_without_selecting' => 0, + 'gtk_entry_im_context_filter_keypress' => 0, + 'gtk_entry_set_alignment' => 0, + 'gtk_entry_set_buffer' => 0, + 'gtk_entry_set_icon_from_icon_name' => 0, + 'gtk_entry_set_text' => 0, + 'gtk_entry_set_visibility' => 0, + 'gtk_event_box_get_type' => 0, + 'gtk_event_controller_reset' => 0, + 'gtk_flow_box_bind_model' => 0, + 'gtk_flow_box_child_get_type' => 0, + 'gtk_flow_box_get_child_at_index' => 0, + 'gtk_gesture_get_last_event' => 0, + 'gtk_gesture_long_press_get_type' => 0, + 'gtk_gesture_set_sequence_state' => 0, + 'gtk_gesture_set_state' => 0, + 'gtk_gesture_single_get_current_sequence' => 0, + 'gtk_gesture_single_get_type' => 0, + 'gtk_grid_attach' => 0, + 'gtk_grid_get_child_at' => 0, + 'gtk_grid_get_type' => 0, + 'gtk_grid_remove_row' => 0, + 'gtk_icon_size_get_type' => 0, + 'gtk_icon_theme_add_resource_path' => 0, + 'gtk_icon_theme_get_default' => 0, + 'gtk_image_get_type' => 0, + 'gtk_image_new_from_gicon' => 0, + 'gtk_image_new_from_icon_name' => 0, + 'gtk_image_set_from_gicon' => 0, + 'gtk_image_set_from_icon_name' => 0, + 'gtk_image_set_pixel_size' => 0, + 'gtk_label_get_label' => 0, + 'gtk_label_get_text' => 0, + 'gtk_label_get_type' => 0, + 'gtk_label_new' => 0, + 'gtk_label_set_attributes' => 0, + 'gtk_label_set_label' => 0, + 'gtk_label_set_line_wrap' => 0, + 'gtk_label_set_single_line_mode' => 0, + 'gtk_label_set_text' => 0, + 'gtk_level_bar_set_max_value' => 0, + 'gtk_level_bar_set_value' => 0, + 'gtk_list_box_bind_model' => 0, + 'gtk_list_box_get_row_at_y' => 0, + 'gtk_list_box_get_type' => 0, + 'gtk_list_box_insert' => 0, + 'gtk_list_box_row_get_index' => 0, + 'gtk_list_box_row_get_type' => 0, + 'gtk_list_box_row_set_activatable' => 0, + 'gtk_list_box_row_set_header' => 0, + 'gtk_list_box_row_set_selectable' => 0, + 'gtk_list_box_set_header_func' => 0, + 'gtk_main_quit' => 0, + 'gtk_orientable_get_type' => 0, + 'gtk_overlay_get_type' => 0, + 'gtk_popover_bind_model' => 0, + 'gtk_popover_popdown' => 0, + 'gtk_popover_popup' => 0, + 'gtk_progress_bar_set_fraction' => 0, + 'gtk_range_get_value' => 0, + 'gtk_range_set_adjustment' => 0, + 'gtk_range_set_range' => 0, + 'gtk_range_set_value' => 0, + 'gtk_render_background' => 0, + 'gtk_revealer_get_child_revealed' => 0, + 'gtk_revealer_get_reveal_child' => 0, + 'gtk_revealer_set_reveal_child' => 0, + 'gtk_revealer_set_transition_duration' => 0, + 'gtk_revealer_set_transition_type' => 0, + 'gtk_revealer_transition_type_get_type' => 0, + 'gtk_scale_add_mark' => 0, + 'gtk_scale_clear_marks' => 0, + 'gtk_scrolled_window_get_vadjustment' => 0, + 'gtk_search_entry_handle_event' => 0, + 'gtk_separator_new' => 0, + 'gtk_settings_get_default' => 0, + 'gtk_show_uri_on_window' => 0, + 'gtk_size_group_add_widget' => 0, + 'gtk_spinner_start' => 0, + 'gtk_spinner_stop' => 0, + 'gtk_stack_get_type' => 0, + 'gtk_stack_get_visible_child' => 0, + 'gtk_stack_set_visible_child' => 0, + 'gtk_stack_set_visible_child_name' => 0, + 'gtk_style_context_add_class' => 0, + 'gtk_style_context_add_provider_for_screen' => 0, + 'gtk_style_context_get_color' => 0, + 'gtk_style_context_remove_class' => 0, + 'gtk_style_context_remove_provider_for_screen' => 0, + 'gtk_switch_get_active' => 0, + 'gtk_switch_get_state' => 0, + 'gtk_switch_get_type' => 0, + 'gtk_switch_set_active' => 0, + 'gtk_toggle_button_get_active' => 0, + 'gtk_toggle_button_set_active' => 0, + 'gtk_widget_activate' => 0, + 'gtk_widget_add_events' => 0, + 'gtk_widget_add_tick_callback' => 0, + 'gtk_widget_class_bind_template_callback_full' => 0, + 'gtk_widget_class_bind_template_child_full' => 0, + 'gtk_widget_class_set_accessible_role' => 0, + 'gtk_widget_class_set_css_name' => 0, + 'gtk_widget_class_set_template_from_resource' => 0, + 'gtk_widget_destroy' => 0, + 'gtk_widget_destroyed' => 0, + 'gtk_widget_error_bell' => 0, + 'gtk_widget_get_action_group' => 0, + 'gtk_widget_get_allocated_height' => 0, + 'gtk_widget_get_allocated_width' => 0, + 'gtk_widget_get_allocation' => 0, + 'gtk_widget_get_can_focus' => 0, + 'gtk_widget_get_clip' => 0, + 'gtk_widget_get_direction' => 0, + 'gtk_widget_get_display' => 0, + 'gtk_widget_get_frame_clock' => 0, + 'gtk_widget_get_mapped' => 0, + 'gtk_widget_get_margin_bottom' => 0, + 'gtk_widget_get_margin_end' => 0, + 'gtk_widget_get_margin_start' => 0, + 'gtk_widget_get_margin_top' => 0, + 'gtk_widget_get_parent' => 0, + 'gtk_widget_get_preferred_height' => 0, + 'gtk_widget_get_preferred_height_and_baseline_for_width' => 0, + 'gtk_widget_get_preferred_height_for_width' => 0, + 'gtk_widget_get_preferred_width' => 0, + 'gtk_widget_get_preferred_width_for_height' => 0, + 'gtk_widget_get_scale_factor' => 0, + 'gtk_widget_get_sensitive' => 0, + 'gtk_widget_get_state_flags' => 0, + 'gtk_widget_get_style_context' => 0, + 'gtk_widget_get_toplevel' => 0, + 'gtk_widget_get_type' => 0, + 'gtk_widget_get_visible' => 0, + 'gtk_widget_get_window' => 0, + 'gtk_widget_grab_default' => 0, + 'gtk_widget_grab_focus' => 0, + 'gtk_widget_has_focus' => 0, + 'gtk_widget_hide' => 0, + 'gtk_widget_init_template' => 0, + 'gtk_widget_input_shape_combine_region' => 0, + 'gtk_widget_insert_action_group' => 0, + 'gtk_widget_is_visible' => 0, + 'gtk_widget_queue_allocate' => 0, + 'gtk_widget_queue_draw' => 0, + 'gtk_widget_queue_resize' => 0, + 'gtk_widget_remove_tick_callback' => 0, + 'gtk_widget_set_allocation' => 0, + 'gtk_widget_set_clip' => 0, + 'gtk_widget_set_direction' => 0, + 'gtk_widget_set_has_window' => 0, + 'gtk_widget_set_hexpand' => 0, + 'gtk_widget_set_margin_bottom' => 0, + 'gtk_widget_set_margin_end' => 0, + 'gtk_widget_set_margin_start' => 0, + 'gtk_widget_set_margin_top' => 0, + 'gtk_widget_set_opacity' => 0, + 'gtk_widget_set_parent' => 0, + 'gtk_widget_set_sensitive' => 0, + 'gtk_widget_set_state_flags' => 0, + 'gtk_widget_set_valign' => 0, + 'gtk_widget_set_visible' => 0, + 'gtk_widget_show' => 0, + 'gtk_widget_size_allocate' => 0, + 'gtk_widget_translate_coordinates' => 0, + 'gtk_widget_unparent' => 0, + 'gtk_widget_unset_state_flags' => 0, + 'gtk_window_get_size' => 0, + 'gtk_window_get_type' => 0, + 'gtk_window_present' => 0, + 'gtk_window_resize' => 0, + 'gtk_window_set_decorated' => 0, + 'gtk_window_set_has_user_ref_count' => 0, + 'gtk_window_set_title' => 0, + 'hdy_action_row_get_activatable_widget@LIBHANDY_1_0' => 0, + 'hdy_action_row_get_type@LIBHANDY_1_0' => 0, + 'hdy_action_row_set_activatable_widget@LIBHANDY_1_0' => 0, + 'hdy_action_row_set_subtitle@LIBHANDY_1_0' => 0, + 'hdy_avatar_set_loadable_icon@LIBHANDY_1_0' => 0, + 'hdy_avatar_set_show_initials@LIBHANDY_1_0' => 0, + 'hdy_avatar_set_size@LIBHANDY_1_0' => 0, + 'hdy_avatar_set_text@LIBHANDY_1_0' => 0, + 'hdy_carousel_get_position@LIBHANDY_1_0' => 0, + 'hdy_carousel_get_type@LIBHANDY_1_0' => 0, + 'hdy_carousel_insert@LIBHANDY_1_0' => 0, + 'hdy_carousel_scroll_to@LIBHANDY_1_0' => 0, + 'hdy_combo_row_bind_name_model@LIBHANDY_1_0' => 0, + 'hdy_combo_row_get_model@LIBHANDY_1_0' => 0, + 'hdy_combo_row_get_selected_index@LIBHANDY_1_0' => 0, + 'hdy_combo_row_get_type@LIBHANDY_1_0' => 0, + 'hdy_combo_row_new@LIBHANDY_1_0' => 0, + 'hdy_combo_row_set_selected_index@LIBHANDY_1_0' => 0, + 'hdy_deck_get_can_swipe_back@LIBHANDY_1_0' => 0, + 'hdy_deck_get_can_swipe_forward@LIBHANDY_1_0' => 0, + 'hdy_deck_get_transition_running@LIBHANDY_1_0' => 0, + 'hdy_deck_get_type@LIBHANDY_1_0' => 0, + 'hdy_deck_get_visible_child@LIBHANDY_1_0' => 0, + 'hdy_deck_navigate@LIBHANDY_1_0' => 0, + 'hdy_deck_set_can_swipe_back@LIBHANDY_1_0' => 0, + 'hdy_deck_set_can_swipe_forward@LIBHANDY_1_0' => 0, + 'hdy_deck_set_transition_duration@LIBHANDY_1_0' => 0, + 'hdy_deck_set_visible_child@LIBHANDY_1_0' => 0, + 'hdy_deck_set_visible_child_name@LIBHANDY_1_0' => 0, + 'hdy_ease_out_cubic@LIBHANDY_1_0' => 0, + 'hdy_get_enable_animations@LIBHANDY_1_0' => 0, + 'hdy_preferences_row_set_title@LIBHANDY_1_0' => 0, + 'hdy_status_page_new@LIBHANDY_1_0' => 0, + 'hdy_status_page_set_description@LIBHANDY_1_0' => 0, + 'hdy_status_page_set_icon_name@LIBHANDY_1_0' => 0, + 'hdy_status_page_set_title@LIBHANDY_1_0' => 0, + 'hdy_swipe_tracker_new@LIBHANDY_1_0' => 0, + 'hdy_swipe_tracker_set_allow_mouse_drag@LIBHANDY_1_0' => 0, + 'hdy_swipe_tracker_set_reversed@LIBHANDY_1_0' => 0, + 'hdy_swipeable_get_type@LIBHANDY_1_0' => 0, + 'lfb_event_end_feedback_async@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_get_event@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_get_state@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_new@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_set_app_id@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_set_feedback_profile@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_set_important@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_set_sound_file@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_trigger_feedback_async@LIBFEEDBACK_0_0_0' => 0, + 'lfb_event_trigger_feedback_finish@LIBFEEDBACK_0_0_0' => 0, + 'lfb_get_feedback_profile@LIBFEEDBACK_0_0_0' => 0, + 'lfb_get_proxy@LIBFEEDBACK_0_0_0' => 0, + 'lfb_init@LIBFEEDBACK_0_0_0' => 0, + 'lfb_is_initted@LIBFEEDBACK_0_0_0' => 0, + 'lfb_set_feedback_profile@LIBFEEDBACK_0_0_0' => 0, + 'lfb_uninit@LIBFEEDBACK_0_0_0' => 0, + 'localtime_r@GLIBC_2.2.5' => 0, + 'log10@GLIBC_2.2.5' => 0, + 'memcmp@GLIBC_2.2.5' => 0, + 'memcpy@GLIBC_2.14' => 0, + 'memfd_create@GLIBC_2.27' => 0, + 'memmove@GLIBC_2.2.5' => 0, + 'mm_cbm_get_channel' => 0, + 'mm_cbm_get_path' => 0, + 'mm_cbm_get_state' => 0, + 'mm_cbm_get_text' => 0, + 'mm_cbm_get_type' => 0, + 'mm_manager_new' => 0, + 'mm_manager_new_finish' => 0, + 'mm_modem_3gpp_get_operator_name' => 0, + 'mm_modem_3gpp_get_type' => 0, + 'mm_modem_cell_broadcast_get_type' => 0, + 'mm_modem_cell_broadcast_list' => 0, + 'mm_modem_cell_broadcast_list_finish' => 0, + 'mm_modem_get_access_technologies' => 0, + 'mm_modem_get_signal_quality' => 0, + 'mm_modem_get_sim_path' => 0, + 'mm_modem_get_state' => 0, + 'mm_modem_get_unlock_required' => 0, + 'mm_object_get_modem' => 0, + 'mm_object_get_modem_3gpp' => 0, + 'mm_object_get_modem_cell_broadcast' => 0, + 'mm_object_peek_modem_3gpp' => 0, + 'mm_object_peek_modem_cell_broadcast' => 0, + 'mmap64@GLIBC_2.2.5' => 0, + 'munmap@GLIBC_2.2.5' => 0, + 'nm_802_11_mode_get_type@libnm_1_0_0' => 0, + 'nm_access_point_filter_connections@libnm_1_0_0' => 0, + 'nm_access_point_get_bssid@libnm_1_0_0' => 0, + 'nm_access_point_get_flags@libnm_1_0_0' => 0, + 'nm_access_point_get_mode@libnm_1_0_0' => 0, + 'nm_access_point_get_ssid@libnm_1_0_0' => 0, + 'nm_access_point_get_strength@libnm_1_0_0' => 0, + 'nm_access_point_get_type@libnm_1_0_0' => 0, + 'nm_active_connection_get_connection@libnm_1_0_0' => 0, + 'nm_active_connection_get_connection_type@libnm_1_0_0' => 0, + 'nm_active_connection_get_devices@libnm_1_0_0' => 0, + 'nm_active_connection_get_id@libnm_1_0_0' => 0, + 'nm_active_connection_get_state@libnm_1_0_0' => 0, + 'nm_active_connection_get_state_reason@libnm_1_8_0' => 0, + 'nm_active_connection_get_type@libnm_1_0_0' => 0, + 'nm_active_connection_get_uuid@libnm_1_0_0' => 0, + 'nm_active_connection_get_vpn@libnm_1_0_0' => 0, + 'nm_active_connection_state_get_type@libnm_1_0_0' => 0, + 'nm_client_activate_connection_async@libnm_1_0_0' => 0, + 'nm_client_activate_connection_finish@libnm_1_0_0' => 0, + 'nm_client_add_and_activate_connection_async@libnm_1_0_0' => 0, + 'nm_client_add_and_activate_connection_finish@libnm_1_0_0' => 0, + 'nm_client_connectivity_check_get_uri@libnm_1_20_0' => 0, + 'nm_client_dbus_set_property@libnm_1_24_0' => 0, + 'nm_client_deactivate_connection_async@libnm_1_0_0' => 0, + 'nm_client_deactivate_connection_finish@libnm_1_0_0' => 0, + 'nm_client_get_active_connections@libnm_1_0_0' => 0, + 'nm_client_get_connection_by_uuid@libnm_1_0_0' => 0, + 'nm_client_get_connections@libnm_1_0_0' => 0, + 'nm_client_get_connectivity@libnm_1_0_0' => 0, + 'nm_client_get_devices@libnm_1_0_0' => 0, + 'nm_client_get_type@libnm_1_0_0' => 0, + 'nm_client_new_async@libnm_1_0_0' => 0, + 'nm_client_new_finish@libnm_1_0_0' => 0, + 'nm_client_wireless_get_enabled@libnm_1_0_0' => 0, + 'nm_connection_add_setting@libnm_1_0_0' => 0, + 'nm_connection_for_each_setting_value@libnm_1_0_0' => 0, + 'nm_connection_get_connection_type@libnm_1_0_0' => 0, + 'nm_connection_get_id@libnm_1_0_0' => 0, + 'nm_connection_get_setting@libnm_1_0_0' => 0, + 'nm_connection_get_setting_by_name@libnm_1_0_0' => 0, + 'nm_connection_get_setting_connection@libnm_1_0_0' => 0, + 'nm_connection_get_setting_ip4_config@libnm_1_0_0' => 0, + 'nm_connection_get_setting_vpn@libnm_1_0_0' => 0, + 'nm_connection_get_setting_wireless@libnm_1_0_0' => 0, + 'nm_connection_get_setting_wireless_security@libnm_1_0_0' => 0, + 'nm_connection_get_type@libnm_1_0_0' => 0, + 'nm_connection_get_uuid@libnm_1_0_0' => 0, + 'nm_connection_is_type@libnm_1_0_0' => 0, + 'nm_connection_update_secrets@libnm_1_0_0' => 0, + 'nm_device_connection_compatible@libnm_1_0_0' => 0, + 'nm_device_filter_connections@libnm_1_0_0' => 0, + 'nm_device_wifi_get_access_points@libnm_1_0_0' => 0, + 'nm_device_wifi_get_active_access_point@libnm_1_0_0' => 0, + 'nm_device_wifi_get_capabilities@libnm_1_0_0' => 0, + 'nm_device_wifi_get_last_scan@libnm_1_12_0' => 0, + 'nm_device_wifi_get_type@libnm_1_0_0' => 0, + 'nm_device_wifi_request_scan_async@libnm_1_0_0' => 0, + 'nm_device_wifi_request_scan_finish@libnm_1_0_0' => 0, + 'nm_object_get_path@libnm_1_0_0' => 0, + 'nm_remote_connection_commit_changes_async@libnm_1_0_0' => 0, + 'nm_remote_connection_commit_changes_finish@libnm_1_0_0' => 0, + 'nm_secret_agent_error_quark@libnm_1_0_0' => 0, + 'nm_secret_agent_old_delete_secrets@libnm_1_0_0' => 0, + 'nm_secret_agent_old_get_type@libnm_1_0_0' => 0, + 'nm_secret_agent_old_register_async@libnm_1_0_0' => 0, + 'nm_secret_agent_old_register_finish@libnm_1_0_0' => 0, + 'nm_secret_agent_old_save_secrets@libnm_1_0_0' => 0, + 'nm_setting_802_1x_get_type@libnm_1_0_0' => 0, + 'nm_setting_connection_get_connection_type@libnm_1_0_0' => 0, + 'nm_setting_connection_get_id@libnm_1_0_0' => 0, + 'nm_setting_connection_get_timestamp@libnm_1_0_0' => 0, + 'nm_setting_connection_get_type@libnm_1_0_0' => 0, + 'nm_setting_connection_get_uuid@libnm_1_0_0' => 0, + 'nm_setting_connection_new@libnm_1_0_0' => 0, + 'nm_setting_enumerate_values@libnm_1_0_0' => 0, + 'nm_setting_get_name@libnm_1_0_0' => 0, + 'nm_setting_get_secret_flags@libnm_1_0_0' => 0, + 'nm_setting_ip4_config_new@libnm_1_0_0' => 0, + 'nm_setting_ip6_config_new@libnm_1_0_0' => 0, + 'nm_setting_ip_config_get_method@libnm_1_0_0' => 0, + 'nm_setting_pppoe_get_type@libnm_1_0_0' => 0, + 'nm_setting_proxy_new@libnm_1_6_0' => 0, + 'nm_setting_vpn_foreach_data_item@libnm_1_0_0' => 0, + 'nm_setting_vpn_foreach_secret@libnm_1_0_0' => 0, + 'nm_setting_vpn_get_service_type@libnm_1_0_0' => 0, + 'nm_setting_vpn_get_type@libnm_1_0_0' => 0, + 'nm_setting_wired_get_type@libnm_1_0_0' => 0, + 'nm_setting_wireless_get_mode@libnm_1_0_0' => 0, + 'nm_setting_wireless_get_ssid@libnm_1_0_0' => 0, + 'nm_setting_wireless_get_type@libnm_1_0_0' => 0, + 'nm_setting_wireless_new@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_add_group@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_add_pairwise@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_add_proto@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_auth_alg@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_key_mgmt@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_num_protos@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_proto@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_psk@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_type@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_wep_key@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_get_wep_tx_keyidx@libnm_1_0_0' => 0, + 'nm_setting_wireless_security_new@libnm_1_0_0' => 0, + 'nm_simple_connection_new@libnm_1_0_0' => 0, + 'nm_simple_connection_new_clone@libnm_1_0_0' => 0, + 'nm_utils_ssid_to_utf8@libnm_1_0_0' => 0, + 'nm_utils_wep_key_valid@libnm_1_0_0' => 0, + 'nm_utils_wpa_psk_valid@libnm_1_0_0' => 0, + 'nm_vpn_plugin_info_get_auth_dialog@libnm_1_4_0' => 0, + 'nm_vpn_plugin_info_lookup_property@libnm_1_2_0' => 0, + 'nm_vpn_plugin_info_new_search_file@libnm_1_4_0' => 0, + 'open64@GLIBC_2.2.5' => 0, + 'pa_channel_map_can_balance@PULSE_0' => 0, + 'pa_channel_map_can_fade@PULSE_0' => 0, + 'pa_channel_map_has_position@PULSE_0' => 0, + 'pa_channel_map_snprint@PULSE_0' => 0, + 'pa_channel_map_to_pretty_name@PULSE_0' => 0, + 'pa_channel_map_valid@PULSE_0' => 0, + 'pa_context_connect@PULSE_0' => 0, + 'pa_context_disconnect@PULSE_0' => 0, + 'pa_context_errno@PULSE_0' => 0, + 'pa_context_get_card_info_by_index@PULSE_0' => 0, + 'pa_context_get_card_info_list@PULSE_0' => 0, + 'pa_context_get_client_info@PULSE_0' => 0, + 'pa_context_get_client_info_list@PULSE_0' => 0, + 'pa_context_get_server_info@PULSE_0' => 0, + 'pa_context_get_server_protocol_version@PULSE_0' => 0, + 'pa_context_get_sink_info_by_index@PULSE_0' => 0, + 'pa_context_get_sink_info_list@PULSE_0' => 0, + 'pa_context_get_sink_input_info@PULSE_0' => 0, + 'pa_context_get_sink_input_info_list@PULSE_0' => 0, + 'pa_context_get_source_info_by_index@PULSE_0' => 0, + 'pa_context_get_source_info_list@PULSE_0' => 0, + 'pa_context_get_source_output_info@PULSE_0' => 0, + 'pa_context_get_source_output_info_list@PULSE_0' => 0, + 'pa_context_get_state@PULSE_0' => 0, + 'pa_context_new_with_proplist@PULSE_0' => 0, + 'pa_context_set_card_profile_by_index@PULSE_0' => 0, + 'pa_context_set_default_sink@PULSE_0' => 0, + 'pa_context_set_default_source@PULSE_0' => 0, + 'pa_context_set_sink_input_mute@PULSE_0' => 0, + 'pa_context_set_sink_input_volume@PULSE_0' => 0, + 'pa_context_set_sink_mute_by_index@PULSE_0' => 0, + 'pa_context_set_sink_port_by_index@PULSE_0' => 0, + 'pa_context_set_sink_volume_by_index@PULSE_0' => 0, + 'pa_context_set_source_mute_by_index@PULSE_0' => 0, + 'pa_context_set_source_output_mute@PULSE_0' => 0, + 'pa_context_set_source_output_volume@PULSE_0' => 0, + 'pa_context_set_source_port_by_index@PULSE_0' => 0, + 'pa_context_set_source_volume_by_index@PULSE_0' => 0, + 'pa_context_set_state_callback@PULSE_0' => 0, + 'pa_context_set_subscribe_callback@PULSE_0' => 0, + 'pa_context_subscribe@PULSE_0' => 0, + 'pa_context_unref@PULSE_0' => 0, + 'pa_cvolume_compatible_with_channel_map@PULSE_0' => 0, + 'pa_cvolume_equal@PULSE_0' => 0, + 'pa_cvolume_get_balance@PULSE_0' => 0, + 'pa_cvolume_get_fade@PULSE_0' => 0, + 'pa_cvolume_get_position@PULSE_0' => 0, + 'pa_cvolume_max@PULSE_0' => 0, + 'pa_cvolume_scale@PULSE_0' => 0, + 'pa_cvolume_set@PULSE_0' => 0, + 'pa_ext_stream_restore_read@PULSE_0' => 0, + 'pa_ext_stream_restore_set_subscribe_cb@PULSE_0' => 0, + 'pa_ext_stream_restore_subscribe@PULSE_0' => 0, + 'pa_ext_stream_restore_write@PULSE_0' => 0, + 'pa_glib_mainloop_free@PULSE_0' => 0, + 'pa_glib_mainloop_get_api@PULSE_0' => 0, + 'pa_glib_mainloop_new@PULSE_0' => 0, + 'pa_operation_cancel@PULSE_0' => 0, + 'pa_operation_get_state@PULSE_0' => 0, + 'pa_operation_unref@PULSE_0' => 0, + 'pa_proplist_free@PULSE_0' => 0, + 'pa_proplist_gets@PULSE_0' => 0, + 'pa_proplist_iterate@PULSE_0' => 0, + 'pa_proplist_new@PULSE_0' => 0, + 'pa_proplist_sets@PULSE_0' => 0, + 'pa_strerror@PULSE_0' => 0, + 'pa_sw_volume_from_dB@PULSE_0' => 0, + 'pa_sw_volume_to_dB@PULSE_0' => 0, + 'pam_acct_mgmt@LIBPAM_1.0' => 0, + 'pam_authenticate@LIBPAM_1.0' => 0, + 'pam_end@LIBPAM_1.0' => 0, + 'pam_start@LIBPAM_1.0' => 0, + 'pam_strerror@LIBPAM_1.0' => 0, + 'pango_attr_font_features_new' => 0, + 'pango_attr_list_insert' => 0, + 'pango_attr_list_new' => 0, + 'pango_attr_list_unref' => 0, + 'pango_parse_markup' => 0, + 'polkit_agent_listener_get_type' => 0, + 'polkit_agent_listener_register' => 0, + 'polkit_agent_listener_unregister' => 0, + 'polkit_agent_session_cancel' => 0, + 'polkit_agent_session_initiate' => 0, + 'polkit_agent_session_new' => 0, + 'polkit_agent_session_response' => 0, + 'polkit_error_quark' => 0, + 'polkit_unix_session_new_for_process_sync' => 0, + 'polkit_unix_user_get_type' => 0, + 'polkit_unix_user_get_uid' => 0, + 'polkit_unix_user_new_for_name' => 0, + 'read@GLIBC_2.2.5' => 0, + 'realpath@GLIBC_2.3' => 0, + 'round@GLIBC_2.2.5' => 0, + 'sd_pid_get_session@LIBSYSTEMD_209' => 0, + 'sd_uid_get_display@LIBSYSTEMD_213' => 0, + 'secret_attributes_build' => 0, + 'secret_item_get_attributes' => 0, + 'secret_item_get_secret' => 0, + 'secret_password_clear' => 0, + 'secret_password_clear_finish' => 0, + 'secret_password_storev' => 0, + 'secret_service_search' => 0, + 'secret_service_search_finish' => 0, + 'secret_value_get' => 0, + 'secret_value_unref' => 0, + 'setlocale@GLIBC_2.2.5' => 0, + 'sin@GLIBC_2.2.5' => 0, + 'soup_uri_decode_data_uri' => 0, + 'sqrt@GLIBC_2.2.5' => 0, + 'strcasestr@GLIBC_2.2.5' => 0, + 'strchr@GLIBC_2.2.5' => 0, + 'strcmp@GLIBC_2.2.5' => 0, + 'strerror@GLIBC_2.2.5' => 0, + 'strftime@GLIBC_2.2.5' => 0, + 'strlen@GLIBC_2.2.5' => 0, + 'strrchr@GLIBC_2.2.5' => 0, + 'strstr@GLIBC_2.2.5' => 0, + 'strtol@GLIBC_2.2.5' => 0, + 'time@GLIBC_2.2.5' => 0, + 'up_client_get_display_device' => 0, + 'up_client_new_async' => 0, + 'up_client_new_finish' => 0, + 'wl_buffer_interface' => 0, + 'wl_display_roundtrip' => 0, + 'wl_output_interface' => 0, + 'wl_proxy_add_listener' => 0, + 'wl_proxy_destroy' => 0, + 'wl_proxy_get_version' => 0, + 'wl_proxy_marshal_flags' => 0, + 'wl_proxy_set_user_data' => 0, + 'wl_registry_interface' => 0, + 'wl_seat_interface' => 0, + 'wl_shm_interface' => 0, + 'wl_shm_pool_interface' => 0, + 'wl_surface_interface' => 0 + } + }, + 'WordSize' => '8' + }; diff --git a/src/libphosh.h b/src/libphosh.h new file mode 100644 index 000000000..7cb4d2b32 --- /dev/null +++ b/src/libphosh.h @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +/* + * The list of headers here must match what is exposed in Phosh-0.gir + * for Rust binding generation. + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define __LIBPHOSH_H_INSIDE__ + +#include "layersurface.h" +#include "lockscreen-manager.h" +#include "lockscreen.h" +#include "quick-setting.h" +#include "screenshot-manager.h" +#include "shell.h" +#include "status-icon.h" +#include "status-page.h" +#include "wall-clock.h" + +G_END_DECLS diff --git a/src/libphosh.syms.in b/src/libphosh.syms.in new file mode 100644 index 000000000..584b61a96 --- /dev/null +++ b/src/libphosh.syms.in @@ -0,0 +1,39 @@ + phosh_dbus_screenshot_get_type; + phosh_dbus_screenshot_proxy_get_type; + phosh_dbus_screenshot_skeleton_get_type; + phosh_layer_surface_get_type; + phosh_lockscreen_add_extra_page; + phosh_lockscreen_clear_pin_entry; + phosh_lockscreen_get_page; + phosh_lockscreen_get_pin_entry; + phosh_lockscreen_get_type; + phosh_lockscreen_manager_get_active_time; + phosh_lockscreen_manager_get_locked; + phosh_lockscreen_manager_get_lockscreen; + phosh_lockscreen_manager_get_page; + phosh_lockscreen_manager_get_type; + phosh_lockscreen_manager_set_locked; + phosh_lockscreen_manager_set_page; + phosh_lockscreen_page_get_type; + phosh_lockscreen_set_default_page; + phosh_lockscreen_set_page; + phosh_lockscreen_set_unlock_status; + phosh_lockscreen_shake_pin_entry; + phosh_screenshot_manager_get_type; + phosh_screenshot_manager_new; + phosh_screenshot_manager_take_screenshot; + phosh_shell_fade_out; + phosh_shell_get_lockscreen_manager; + phosh_shell_get_lockscreen_type; + phosh_shell_get_screenshot_manager; + phosh_shell_get_type; + phosh_shell_get_usable_area; + phosh_shell_new; + phosh_shell_set_default; + phosh_wall_clock_get_clock; + phosh_wall_clock_get_default; + phosh_wall_clock_get_type; + phosh_wall_clock_local_date; + phosh_wall_clock_new; + phosh_wall_clock_set_default; + phosh_wall_clock_string_for_datetime; diff --git a/src/location-info.c b/src/location-info.c new file mode 100644 index 000000000..488bc4314 --- /dev/null +++ b/src/location-info.c @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-location-info" + +#include "phosh-config.h" + +#include "location-info.h" +#include "location-manager.h" + +#include "shell-priv.h" + +/** + * PhoshLocationInfo: + * + * A widget to display the location service status + * + * #PhoshLocationInfo indicates if the location service is active. + * The widgets container should hide the widget if + * #PhoshLocationInfo:active is %FALSE. + */ + +enum { + PROP_0, + PROP_ACTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshLocationInfo { + PhoshStatusIcon parent; + + gboolean active; + PhoshLocationManager *manager; +}; +G_DEFINE_TYPE (PhoshLocationInfo, phosh_location_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_location_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLocationInfo *self = PHOSH_LOCATION_INFO (object); + + switch (property_id) { + case PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_location_info_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLocationInfo *self = PHOSH_LOCATION_INFO (object); + + switch (property_id) { + case PROP_ACTIVE: + self->active = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static gboolean +active_to_icon_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean active = g_value_get_boolean (from_value); + + g_value_set_string (to_value, active ? "location-services-active-symbolic" : + "location-services-disabled-symbolic"); + return TRUE; +} + + +static void +phosh_location_info_constructed (GObject *object) +{ + PhoshLocationInfo *self = PHOSH_LOCATION_INFO (object); + PhoshShell *shell = phosh_shell_get_default ();; + + self->manager = g_object_ref (phosh_shell_get_location_manager (shell)); + + g_object_bind_property (self->manager, "active", + self,"active", + G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (self->manager, "active", + self,"icon-name", + G_BINDING_SYNC_CREATE, + active_to_icon_name, + NULL, NULL, NULL); + + G_OBJECT_CLASS (phosh_location_info_parent_class)->constructed (object); +} + + +static void +phosh_location_info_dispose (GObject *object) +{ + PhoshLocationInfo *self = PHOSH_LOCATION_INFO (object); + + g_clear_object (&self->manager); + + G_OBJECT_CLASS (phosh_location_info_parent_class)->dispose (object); +} + + +static void +phosh_location_info_class_init (PhoshLocationInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_location_info_constructed; + object_class->dispose = phosh_location_info_dispose; + object_class->get_property = phosh_location_info_get_property; + object_class->set_property = phosh_location_info_set_property; + + gtk_widget_class_set_css_name (widget_class, "phosh-location-info"); + + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", + "Active", + "Whether the location service is active (in use)", + FALSE, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_location_info_init (PhoshLocationInfo *self) +{ +} + + +GtkWidget * +phosh_location_info_new (void) +{ + return g_object_new (PHOSH_TYPE_LOCATION_INFO, NULL); +} diff --git a/src/location-info.h b/src/location-info.h new file mode 100644 index 000000000..c7a63b411 --- /dev/null +++ b/src/location-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LOCATION_INFO (phosh_location_info_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLocationInfo, phosh_location_info, PHOSH, LOCATION_INFO, PhoshStatusIcon) + +GtkWidget * phosh_location_info_new (void); + +G_END_DECLS diff --git a/src/location-manager.c b/src/location-manager.c new file mode 100644 index 000000000..9c3496a19 --- /dev/null +++ b/src/location-manager.c @@ -0,0 +1,526 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-location-manager" + +#include "phosh-config.h" + +#include "app-auth-prompt.h" +#include "geoclue-manager-dbus.h" +#include "location-manager.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include + +/** + * PhoshLocationManager: + * + * Provides the org.freedesktop.GeoClue2.Agent DBus interface + * + * The #PhoshLocationManager provides the agent interface and authorizes + * clients based on the org.gnome.system.location 'enabled' gsetting. Note + * the phosh needs to be enabled as agent in geoclue's config. + */ +#define LOCATION_AGENT_DBUS_NAME "org.freedesktop.GeoClue2.Agent" +#define LOCATION_AGENT_DBUS_PATH "/org/freedesktop/GeoClue2/Agent" + +#define GEOCLUE_SERVICE "org.freedesktop.GeoClue2" +#define GEOCLUE_MANAGER_PATH "/org/freedesktop/GeoClue2/Manager" + +/** + * AccuracyLevel: + * @LEVEL_NONE: Accuracy level unknown or unset. + * @LEVEL_COUNTRY: Country-level accuracy. + * @LEVEL_CITY: City-level accuracy. + * @LEVEL_NEIGHBORHOOD: neighborhood-level accuracy. + * @LEVEL_STREET: Street-level accuracy. + * @LEVEL_EXACT: Exact accuracy. Typically requires GPS receiver. + * + * Used to specify level of accuracy requested by, or allowed for a client. + **/ +typedef enum { + LEVEL_NONE = 0, + LEVEL_COUNTRY = 1, + LEVEL_CITY = 4, + LEVEL_NEIGHBORHOOD = 5, + LEVEL_STREET = 6, + LEVEL_EXACT = 8, +} AccuracyLevel; + + +enum { + PROP_0, + PROP_ENABLED, + PROP_ACTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshLocationManager { + PhoshDBusGeoClue2AgentSkeleton parent; + + PhoshDBusGeoClue2Manager *manager_proxy; + guint dbus_name_id; + GSettings *location_settings; + gboolean enabled; + gboolean active; + GDBusConnection *connection; + + /* Current Request */ + GtkWidget *prompt; + GDBusMethodInvocation *invocation; + AccuracyLevel req_level; + + GCancellable *cancel; +} PhoshLocationManager; + +static void phosh_location_manager_geoclue2_agent_iface_init (PhoshDBusGeoClue2AgentIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshLocationManager, + phosh_location_manager, + PHOSH_DBUS_TYPE_GEO_CLUE2_AGENT_SKELETON, + G_IMPLEMENT_INTERFACE (PHOSH_DBUS_TYPE_GEO_CLUE2_AGENT, + phosh_location_manager_geoclue2_agent_iface_init)); + + +static guint +get_max_level (PhoshLocationManager *self) +{ + gint level = LEVEL_NONE; + + if (self->enabled) { + GDesktopLocationAccuracyLevel val; + + val = g_settings_get_enum (self->location_settings, "max-accuracy-level"); + switch (val) { + case G_DESKTOP_LOCATION_ACCURACY_LEVEL_COUNTRY: + level = LEVEL_COUNTRY; + break; + case G_DESKTOP_LOCATION_ACCURACY_LEVEL_CITY: + level = LEVEL_CITY; + break; + case G_DESKTOP_LOCATION_ACCURACY_LEVEL_NEIGHBORHOOD: + level = LEVEL_NEIGHBORHOOD; + break; + case G_DESKTOP_LOCATION_ACCURACY_LEVEL_STREET: + level = LEVEL_STREET; + break; + case G_DESKTOP_LOCATION_ACCURACY_LEVEL_EXACT: + level = LEVEL_EXACT; + break; + default: + g_warn_if_reached (); + } + } + + return level; +} + + +static void +update_accuracy_level (PhoshLocationManager *self, gboolean enabled) +{ + int level; + + self->enabled = enabled; + level = get_max_level (self); + + g_debug ("Setting accuracy level to %d", level); + phosh_dbus_geo_clue2_agent_set_max_accuracy_level ( + PHOSH_DBUS_GEO_CLUE2_AGENT (self), level); +} + + +static void +phosh_location_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + + switch (property_id) { + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + case PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_location_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + + switch (property_id) { + case PROP_ENABLED: + update_accuracy_level (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_app_auth_prompt_closed (PhoshLocationManager *self, PhoshAppAuthPrompt *prompt) +{ + gboolean grant_access; + + g_return_if_fail (PHOSH_IS_LOCATION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_APP_AUTH_PROMPT (prompt)); + + grant_access = phosh_app_auth_prompt_get_grant_access (GTK_WIDGET (prompt)); + + g_debug ("Granting access for %p at level %d: %s", + self->invocation, + self->req_level, + grant_access ? "yes" : "no"); + + phosh_dbus_geo_clue2_agent_complete_authorize_app (PHOSH_DBUS_GEO_CLUE2_AGENT (self), + self->invocation, + grant_access, + self->req_level); + + /* TODO: save in permission store */ + + self->req_level = LEVEL_NONE; + self->invocation = NULL; + self->prompt = NULL; + + return; +} + + +static gboolean +handle_authorize_app (PhoshDBusGeoClue2Agent *object, + GDBusMethodInvocation *invocation, + const char *arg_desktop_id, + guint arg_req_accuracy_level) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + g_autofree char *desktop_file = NULL; + g_autofree char *body = NULL; + g_autofree char *subtitle = NULL; + + g_autoptr (GDesktopAppInfo) app_info = NULL; + gint level; + + g_debug ("Authorizing %s: %d", arg_desktop_id, self->enabled); + + level = get_max_level (self); + if (arg_req_accuracy_level > level) { + g_debug ("Req accuracy level %d > max allowed %d", arg_req_accuracy_level, level); + phosh_dbus_geo_clue2_agent_complete_authorize_app (object, + invocation, + FALSE, + arg_req_accuracy_level); + return TRUE; + } + + desktop_file = g_strjoin (".", arg_desktop_id, "desktop", NULL); + app_info = g_desktop_app_info_new (desktop_file); + if (app_info == NULL) { + g_debug ("Failed to find %s", desktop_file); + phosh_dbus_geo_clue2_agent_complete_authorize_app (object, + invocation, + FALSE, + arg_req_accuracy_level); + + return TRUE; + } + + /* TODO: look at location permission store */ + + /* Cancel any ongoing prompt */ + if (self->prompt) + gtk_widget_destroy (GTK_WIDGET (self->prompt)); + + if (self->invocation) { + phosh_dbus_geo_clue2_agent_complete_authorize_app ( + object, + self->invocation, + FALSE, + arg_req_accuracy_level); + } + + self->req_level = arg_req_accuracy_level; + self->invocation = invocation; + subtitle = g_strdup_printf (_("Allow '%s' to access your location information?"), + g_app_info_get_display_name (G_APP_INFO (app_info))); + + body = g_desktop_app_info_get_string (app_info, "X-Geoclue-Reason"); + self->prompt = phosh_app_auth_prompt_new (g_app_info_get_icon (G_APP_INFO (app_info)), + _("Geolocation"), + subtitle, body, _("Yes"), _("No"), FALSE, NULL); + g_signal_connect_object (self->prompt, + "closed", + G_CALLBACK (on_app_auth_prompt_closed), + self, + G_CONNECT_SWAPPED); + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + self->prompt, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + + return TRUE; +} + + +static guint +handle_get_max_accuracy_level (PhoshDBusGeoClue2Agent *object) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + gint level = get_max_level (self); + + g_debug ("Accuracy level %d", level); + return level; +} + + +static void +phosh_location_manager_geoclue2_agent_iface_init (PhoshDBusGeoClue2AgentIface *iface) +{ + iface->handle_authorize_app = handle_authorize_app; + iface->get_max_accuracy_level = handle_get_max_accuracy_level; +} + + +static void +on_bus_acquired (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GError) err = NULL; + PhoshLocationManager *self; + GDBusConnection *connection; + + connection = g_bus_get_finish (res, &err); + if (!connection) { + phosh_async_error_warn (err, "Failed to connect to system bus"); + return; + } + + self = PHOSH_LOCATION_MANAGER (user_data); + self->connection = connection; + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + self->connection, + LOCATION_AGENT_DBUS_PATH, + NULL); + update_accuracy_level (self, self->enabled); +} + + +static void +on_add_agent_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + PhoshDBusGeoClue2Manager *manager = PHOSH_DBUS_GEO_CLUE2_MANAGER (source_object); + g_autoptr (GError) err = NULL; + + if (phosh_dbus_geo_clue2_manager_call_add_agent_finish (manager, res, &err)) + g_debug ("Added ourself as geoclue agent"); + else + g_warning ("Failed to add agent: %s", err->message); +} + + +static void +on_agent_in_use_changed (PhoshLocationManager *self) +{ + gboolean in_use; + + g_return_if_fail (PHOSH_IS_LOCATION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_GEO_CLUE2_MANAGER (self->manager_proxy)); + + in_use = phosh_dbus_geo_clue2_manager_get_in_use (self->manager_proxy); + g_debug ("In use: %d", in_use); + + if (in_use == self->active) + return; + + self->active = in_use; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE]); +} + + +static void +on_manager_proxy_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_IS_LOCATION_MANAGER (self)); + + self->manager_proxy = phosh_dbus_geo_clue2_manager_proxy_new_for_bus_finish (res, &err); + if (self->manager_proxy == NULL) { + g_warning ("Failed to create proxy to %s: %s", GEOCLUE_MANAGER_PATH, err->message); + return; + } + + phosh_dbus_geo_clue2_manager_call_add_agent (self->manager_proxy, + /* Agent whitelisted in geoclue conf */ + "sm.puri.Phosh", + self->cancel, + on_add_agent_ready, + NULL); + g_signal_connect_swapped (self->manager_proxy, + "notify::in-use", + G_CALLBACK (on_agent_in_use_changed), + self); + on_agent_in_use_changed (self); +} + + +static void +on_manager_name_appeared (GDBusConnection *connection, + const char *name, + const char *name_owner, + PhoshLocationManager *self) +{ + g_return_if_fail (PHOSH_IS_LOCATION_MANAGER (self)); + g_debug ("%s appeared", name); + + phosh_dbus_geo_clue2_manager_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + GEOCLUE_SERVICE, + GEOCLUE_MANAGER_PATH, + self->cancel, + on_manager_proxy_ready, + self); +} + +static void +on_manager_name_vanished (GDBusConnection *connection, + const char *name, + PhoshLocationManager *self) +{ + g_return_if_fail (PHOSH_IS_LOCATION_MANAGER (self)); + + g_debug ("%s vanished", name); + g_clear_object (&self->manager_proxy); +} + + +static void +phosh_location_manager_dispose (GObject *object) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + /* Close dialog and cancel pending request if ongoing */ + g_clear_pointer (&self->prompt, phosh_cp_widget_destroy); + + if (self->invocation) { + phosh_dbus_geo_clue2_agent_complete_authorize_app (PHOSH_DBUS_GEO_CLUE2_AGENT (self), + self->invocation, + FALSE, + LEVEL_NONE); + self->invocation = NULL; + } + + g_clear_handle_id (&self->dbus_name_id, g_bus_unwatch_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_object (&self->location_settings); + g_clear_object (&self->manager_proxy); + g_clear_object (&self->connection); + + G_OBJECT_CLASS (phosh_location_manager_parent_class)->dispose (object); +} + + +static void +phosh_location_manager_constructed (GObject *object) +{ + PhoshLocationManager *self = PHOSH_LOCATION_MANAGER (object); + + G_OBJECT_CLASS (phosh_location_manager_parent_class)->constructed (object); + + self->location_settings = g_settings_new ("org.gnome.system.location"); + g_settings_bind (self->location_settings, + "enabled", + self, + "enabled", + G_SETTINGS_BIND_DEFAULT); + + g_bus_get (G_BUS_TYPE_SYSTEM, self->cancel, on_bus_acquired, self); + + self->dbus_name_id = g_bus_watch_name (G_BUS_TYPE_SYSTEM, + GEOCLUE_SERVICE, + G_BUS_NAME_WATCHER_FLAGS_NONE, + (GBusNameAppearedCallback) on_manager_name_appeared, + (GBusNameVanishedCallback) on_manager_name_vanished, + self, + NULL); +} + + +static void +phosh_location_manager_class_init (PhoshLocationManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_location_manager_constructed; + object_class->dispose = phosh_location_manager_dispose; + + object_class->set_property = phosh_location_manager_set_property; + object_class->get_property = phosh_location_manager_get_property; + + /** + * LocationManager:enabled: + * + * Whether location services are enabled + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * LocationManager:active: + * + * Whether location services are currently active + */ + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_location_manager_init (PhoshLocationManager *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshLocationManager * +phosh_location_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_LOCATION_MANAGER, NULL); +} diff --git a/src/location-manager.h b/src/location-manager.h new file mode 100644 index 000000000..3a44d14af --- /dev/null +++ b/src/location-manager.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "dbus/geoclue-agent-dbus.h" +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LOCATION_MANAGER (phosh_location_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshLocationManager, phosh_location_manager, PHOSH, LOCATION_MANAGER, + PhoshDBusGeoClue2AgentSkeleton) + +PhoshLocationManager * phosh_location_manager_new (void); + +G_END_DECLS diff --git a/src/lockscreen-bg.c b/src/lockscreen-bg.c new file mode 100644 index 000000000..79c64d1bf --- /dev/null +++ b/src/lockscreen-bg.c @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-lockscreen-bg" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "lockscreen-bg.h" +#include "style-manager.h" +#include "util.h" + +#include + +#include + +#include + +/** + * PhoshLockscreenBg: + * + * The lockscreen's background + */ + +struct _PhoshLockscreenBg { + PhoshLayerSurface parent; + + GdkPixbuf *pixbuf; + PhoshBackgroundImage *bg_image; + + gboolean configured; + gboolean use_background; +}; +G_DEFINE_TYPE (PhoshLockscreenBg, phosh_lockscreen_bg, PHOSH_TYPE_LAYER_SURFACE) + + +static void +update_image (PhoshLockscreenBg *self) +{ + int width, height; + + if (!self->configured) + return; + + width = phosh_layer_surface_get_configured_width (PHOSH_LAYER_SURFACE (self)); + height = phosh_layer_surface_get_configured_height (PHOSH_LAYER_SURFACE (self)); + + g_return_if_fail (width > 0 && height > 0); + + g_debug ("Scaling lockscreen background %p to %dx%d", self, width, height); + + g_clear_object (&self->pixbuf); + if (self->bg_image) { + GdkPixbuf *pixbuf = phosh_background_image_get_pixbuf (self->bg_image); + self->pixbuf = phosh_utils_pixbuf_scale_to_min (pixbuf, width, height); + } + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +static void +phosh_lockscreen_bg_configured (PhoshLayerSurface *layer_surface) +{ + PhoshLockscreenBg *self = PHOSH_LOCKSCREEN_BG (layer_surface); + + PHOSH_LAYER_SURFACE_CLASS (phosh_lockscreen_bg_parent_class)->configured (layer_surface); + + self->configured = TRUE; + update_image (self); +} + + +static gboolean +phosh_lockscreen_bg_draw (GtkWidget *widget, cairo_t *cr) +{ + GtkStyleContext *context; + PhoshLockscreenBg *self = PHOSH_LOCKSCREEN_BG (widget); + int x = 0, y = 0, width, height; + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_BG (self), GDK_EVENT_PROPAGATE); + + if (!self->configured) + return GDK_EVENT_PROPAGATE; + + cairo_save (cr); + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + + width = gtk_widget_get_allocated_width (GTK_WIDGET (self)); + height = gtk_widget_get_allocated_height (GTK_WIDGET (self)); + gtk_render_background (context, cr, 0, 0, width, height); + + if (self->pixbuf && self->use_background) { + gdk_cairo_set_source_pixbuf (cr, self->pixbuf, x, y); + cairo_paint (cr); + } + + cairo_restore (cr); + + return GDK_EVENT_PROPAGATE; +} + + +static void +on_theme_name_changed (PhoshLockscreenBg *self, GParamSpec *pspec, PhoshStyleManager *style_manager) +{ + g_assert (PHOSH_IS_LOCKSCREEN_BG (self)); + g_assert (PHOSH_IS_STYLE_MANAGER (style_manager)); + + self->use_background = !phosh_style_manager_is_high_contrast (style_manager); + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +static void +phosh_lockscreen_bg_finalize (GObject *object) +{ + PhoshLockscreenBg *self = PHOSH_LOCKSCREEN_BG (object); + + g_clear_object (&self->bg_image); + g_clear_object (&self->pixbuf); + + G_OBJECT_CLASS (phosh_lockscreen_bg_parent_class)->finalize (object); +} + + +static void +phosh_lockscreen_bg_class_init (PhoshLockscreenBgClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + + object_class->finalize = phosh_lockscreen_bg_finalize; + + widget_class->draw = phosh_lockscreen_bg_draw; + + layer_surface_class->configured = phosh_lockscreen_bg_configured; + + gtk_widget_class_set_css_name (widget_class, "phosh-lockscreen-bg"); +} + + +static void +phosh_lockscreen_bg_init (PhoshLockscreenBg *self) +{ + PhoshStyleManager *style_manager; + + style_manager = phosh_shell_get_style_manager (phosh_shell_get_default ()); + self->use_background = !phosh_style_manager_is_high_contrast (style_manager); + g_signal_connect_object (style_manager, + "notify::theme-name", + G_CALLBACK (on_theme_name_changed), + self, + G_CONNECT_SWAPPED); +} + + +PhoshLockscreenBg * +phosh_lockscreen_bg_new (struct zwlr_layer_shell_v1 *layer_shell, struct wl_output *wl_output) +{ + return g_object_new (PHOSH_TYPE_LOCKSCREEN_BG, + "layer-shell", layer_shell, + "wl-output", wl_output, + "anchor", (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT), + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", FALSE, + "exclusive-zone", -1, + "namespace", "phosh lockscreen background", + NULL); +} + + +void +phosh_lockscreen_bg_set_image (PhoshLockscreenBg *self, PhoshBackgroundImage *image) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN_BG (self)); + g_return_if_fail (image == NULL || PHOSH_IS_BACKGROUND_IMAGE (image)); + + if (self->bg_image == image) + return; + + g_set_object (&self->bg_image, image); + update_image (self); +} diff --git a/src/lockscreen-bg.h b/src/lockscreen-bg.h new file mode 100644 index 000000000..11a396792 --- /dev/null +++ b/src/lockscreen-bg.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "layersurface-priv.h" +#include "background-image.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_LOCKSCREEN_BG (phosh_lockscreen_bg_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLockscreenBg, phosh_lockscreen_bg, PHOSH, LOCKSCREEN_BG, + PhoshLayerSurface) + +PhoshLockscreenBg * phosh_lockscreen_bg_new (struct zwlr_layer_shell_v1 *layer_shell, + struct wl_output *wl_output); +void phosh_lockscreen_bg_set_image (PhoshLockscreenBg *self, + PhoshBackgroundImage *image); + +G_END_DECLS diff --git a/src/lockscreen-manager-priv.h b/src/lockscreen-manager-priv.h new file mode 100644 index 000000000..5bde0605f --- /dev/null +++ b/src/lockscreen-manager-priv.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "lockscreen-manager.h" +#include "calls-manager.h" + +G_BEGIN_DECLS + +PhoshLockscreenManager *phosh_lockscreen_manager_new (PhoshCallsManager *calls_manager); + +G_END_DECLS diff --git a/src/lockscreen-manager.c b/src/lockscreen-manager.c new file mode 100644 index 000000000..9ba16e1a3 --- /dev/null +++ b/src/lockscreen-manager.c @@ -0,0 +1,657 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2024-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-lockscreen-manager" + +#include "background-cache.h" +#include "background-image.h" +#include "lockscreen-manager-priv.h" +#include "lockscreen-priv.h" +#include "lockshield.h" +#include "monitor-manager.h" +#include "monitor/monitor.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include +#include + +#define SCREENSAVER_SETTINGS "org.gnome.desktop.screensaver" +#define KEY_PICTURE_URI "picture-uri" +#define KEY_PICTURE_OPTIONS "picture-options" + +/** + * PhoshLockscreenManager: + * + * The singleton that manages screen locking + * + * The #PhoshLockscreenManager is responsible for putting the #PhoshLockscreen + * on the primary output and a #PhoshLockshield on other outputs when the session + * becomes idle or when invoked explicitly via phosh_lockscreen_manager_set_locked(). + */ + +enum { + WAKEUP_OUTPUTS, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +enum { + PROP_0, + PROP_LOCKED, + PROP_CALLS_MANAGER, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshLockscreenManager { + GObject parent; + + PhoshLockscreen *lockscreen; /* phone display lock screen */ + GPtrArray *shields; /* other outputs */ + + GSettings *bg_settings; + GFile *bg_file; + GFileMonitor *bg_file_monitor; + GDesktopBackgroundStyle bg_style; + PhoshBackgroundImage *cached_bg_image; + GCancellable *bg_load_cancel; + + gboolean locked; + gboolean locking; + gint64 active_time; /* when lock was activated (in us) */ + + PhoshCallsManager *calls_manager; /* Calls DBus Interface */ +}; + +G_DEFINE_TYPE (PhoshLockscreenManager, phosh_lockscreen_manager, G_TYPE_OBJECT) + + +static void +on_background_cache_fetch_ready (GObject *source_object, GAsyncResult *res, gpointer data) +{ + PhoshLockscreenManager *self = PHOSH_LOCKSCREEN_MANAGER (data); + PhoshBackgroundCache *cache = PHOSH_BACKGROUND_CACHE (source_object); + g_autoptr (PhoshBackgroundImage) image = NULL; + g_autoptr (GError) err = NULL; + + image = phosh_background_cache_fetch_finish (cache, res, &err); + if (!image) { + phosh_async_error_warn (err, "Failed to load background image"); + return; + } + + g_assert (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + g_assert (PHOSH_IS_BACKGROUND_CACHE (cache)); + + g_set_object (&self->cached_bg_image, image); + if (self->lockscreen) + phosh_lockscreen_set_bg_image (self->lockscreen, self->cached_bg_image); +} + + +static void +load_background (PhoshLockscreenManager *self) +{ + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + + g_cancellable_cancel (self->bg_load_cancel); + g_clear_object (&self->bg_load_cancel); + self->bg_load_cancel = g_cancellable_new (); + + phosh_background_cache_fetch_async (cache, + self->bg_file, + self->bg_load_cancel, + on_background_cache_fetch_ready, + self); +} + + +static void +on_file_changed (PhoshLockscreenManager *self, + GFile *file, + GFile *other_file, + GFileMonitorEvent event_type, + GFileMonitor *monitor) +{ + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + + if (event_type != G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT) + return; + + g_debug ("Lockscreen bg file %s changed, clearing cache", g_file_peek_path (self->bg_file)); + phosh_background_cache_remove (cache, self->bg_file); + + load_background (self); +} + + +static void +monitor_bg_file (PhoshLockscreenManager *self) +{ + g_autoptr (GError) err = NULL; + + g_clear_object (&self->bg_file_monitor); + + self->bg_file_monitor = g_file_monitor_file (self->bg_file, G_FILE_MONITOR_NONE, NULL, &err); + if (!self->bg_file_monitor) { + g_autofree char *uri = g_file_get_uri (self->bg_file); + + g_warning ("Failed to setup file monitor for %s: %s", uri, err->message); + return; + } + + g_signal_connect_object (self->bg_file_monitor, + "changed", + G_CALLBACK (on_file_changed), + self, + G_CONNECT_SWAPPED); +} + + +static void +on_picture_params_changed (PhoshLockscreenManager *self) +{ + PhoshBackgroundCache *cache = phosh_background_cache_get_default (); + GDesktopBackgroundStyle style; + g_autoptr (GFile) file = NULL; + g_autofree char *uri = NULL; + + uri = g_settings_get_string (self->bg_settings, KEY_PICTURE_URI); + style = g_settings_get_enum (self->bg_settings, KEY_PICTURE_OPTIONS); + + if (!gm_str_is_null_or_empty (uri) && + g_str_has_prefix (uri, "file:///") && + style != G_DESKTOP_BACKGROUND_STYLE_NONE) { + file = g_file_new_for_uri (uri); + } + + if (phosh_util_file_equal (self->bg_file, file)) + return; + + if (self->bg_file) + phosh_background_cache_remove (cache, self->bg_file); + g_clear_object (&self->bg_file); + g_clear_object (&self->bg_file_monitor); + g_clear_object (&self->cached_bg_image); + + if (!file) + return; + + g_set_object (&self->bg_file, file); + g_debug ("Loading '%s'", g_file_peek_path (self->bg_file)); + monitor_bg_file (self); + load_background (self); +} + + +static void +on_lockscreen_unlock (PhoshLockscreenManager *self, PhoshLockscreen *lockscreen) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitorManager *monitor_manager = phosh_shell_get_monitor_manager (shell); + PhoshMonitor *primary_monitor = phosh_shell_get_primary_monitor (shell); + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (lockscreen)); + g_return_if_fail (lockscreen == PHOSH_LOCKSCREEN (self->lockscreen)); + + g_signal_handlers_disconnect_by_data (monitor_manager, self); + g_signal_handlers_disconnect_by_data (primary_monitor, self); + g_signal_handlers_disconnect_by_data (shell, self); + g_clear_pointer (&self->lockscreen, phosh_cp_widget_destroy); + + /* Unlock all other outputs */ + g_clear_pointer (&self->shields, g_ptr_array_unref); + + self->locked = FALSE; + self->active_time = 0; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOCKED]); +} + + +static void +on_lockscreen_wakeup_output (PhoshLockscreenManager *self, PhoshLockscreen *lockscreen) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN (lockscreen)); + + /* we just proxy the signal here */ + g_signal_emit (self, signals[WAKEUP_OUTPUTS], 0); +} + + +/* Lock a non primary monitor bringing up a shield */ +static void +lock_monitor (PhoshLockscreenManager *self, + PhoshMonitor *monitor) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshWayland *wl = phosh_wayland_get_default (); + GtkWidget *shield; + + /* Primary monitor is handled via on_primary_monitor_changed */ + if (phosh_shell_get_primary_monitor (shell) == monitor) + return; + + g_debug ("Adding shield for %s", monitor->name); + shield = phosh_lockshield_new (phosh_wayland_get_zwlr_layer_shell_v1 (wl), monitor); + + g_object_set_data (G_OBJECT (shield), "phosh-monitor", monitor); + + g_ptr_array_add (self->shields, shield); + gtk_widget_set_visible (shield, TRUE); +} + + +static void +remove_shield_by_monitor (PhoshLockscreenManager *self, + PhoshMonitor *monitor) +{ + if (!self->shields) { + g_debug ("Shields already gone"); + return; + } + + for (int i = 0; i < self->shields->len; i++) { + PhoshMonitor *shield_monitor; + PhoshLockshield *shield = g_ptr_array_index (self->shields, i); + + shield_monitor = g_object_get_data (G_OBJECT (shield), "phosh-monitor"); + g_return_if_fail (PHOSH_IS_MONITOR (shield_monitor)); + if (shield_monitor == monitor) { + g_debug ("Removing shield %p", shield); + g_ptr_array_remove (self->shields, shield); + break; + } + } +} + + +static void +on_monitor_removed (PhoshLockscreenManager *self, + PhoshMonitor *monitor, + PhoshMonitorManager *monitormanager) +{ + + + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + + g_debug ("Monitor '%s' removed", monitor->name); + remove_shield_by_monitor (self, monitor); +} + + +static void +on_monitor_added (PhoshLockscreenManager *self, + PhoshMonitor *monitor, + PhoshMonitorManager *monitormanager) +{ + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + + g_debug ("Monitor '%s' added", monitor->name); + lock_monitor (self, monitor); +} + + +static void +lock_primary_monitor (PhoshLockscreenManager *self) +{ + GType lockscreen_type; + PhoshMonitor *primary_monitor; + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshShell *shell = phosh_shell_get_default (); + + lockscreen_type = phosh_shell_get_lockscreen_type (shell); + primary_monitor = phosh_shell_get_primary_monitor (shell); + g_assert (PHOSH_IS_MONITOR (primary_monitor)); + + /* The primary output gets the clock, keypad, ... */ + self->lockscreen = PHOSH_LOCKSCREEN (phosh_lockscreen_new ( + lockscreen_type, + phosh_wayland_get_zwlr_layer_shell_v1 (wl), + primary_monitor->wl_output, + self->calls_manager)); + g_object_connect (self->lockscreen, + "swapped-object-signal::lockscreen-unlock", on_lockscreen_unlock, self, + "swapped-object-signal::wakeup-output", on_lockscreen_wakeup_output, self, + NULL); + phosh_lockscreen_set_bg_image (self->lockscreen, self->cached_bg_image); + + gtk_widget_set_visible (GTK_WIDGET (self->lockscreen), TRUE); + /* Old lockscreen gets remove due to `layer_surface_closed` */ +} + + +static void +on_primary_monitor_changed (PhoshLockscreenManager *self, + GParamSpec *pspec, + PhoshShell *shell) +{ + PhoshMonitor *monitor; + + g_return_if_fail (PHOSH_IS_SHELL (shell)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + + monitor = phosh_shell_get_primary_monitor (shell); + + if (monitor) { + g_debug ("primary monitor changed to %s, need to move lockscreen", monitor->name); + lock_primary_monitor (self); + /* We don't remove a shield that might exist to avoid the screen + content flickering in. The shield will be removed on unlock */ + } else { + g_debug ("Primary monitor gone, doing nothing"); + } +} + + +static void +lockscreen_lock (PhoshLockscreenManager *self) +{ + PhoshMonitor *primary_monitor; + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitorManager *monitor_manager = phosh_shell_get_monitor_manager (shell); + + g_return_if_fail (!self->locked); + + /* Locking already in progress */ + if (self->locking) + return; + + self->locking = TRUE; + primary_monitor = phosh_shell_get_primary_monitor (shell); + + /* Listen for monitor changes */ + g_signal_connect_object (monitor_manager, "monitor-added", + G_CALLBACK (on_monitor_added), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (monitor_manager, "monitor-removed", + G_CALLBACK (on_monitor_removed), + self, + G_CONNECT_SWAPPED); + + g_signal_connect_object (shell, + "notify::primary-monitor", + G_CALLBACK (on_primary_monitor_changed), + self, + G_CONNECT_SWAPPED); + + if (primary_monitor) + lock_primary_monitor (self); + else + g_message ("No primary monitor to lock"); + + /* Lock all other outputs */ + self->shields = g_ptr_array_new_with_free_func ((GDestroyNotify) (gtk_widget_destroy)); + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (monitor_manager); i++) { + PhoshMonitor *monitor = phosh_monitor_manager_get_monitor (monitor_manager, i); + + if (monitor == NULL || monitor == primary_monitor) + continue; + lock_monitor (self, monitor); + } + + self->locked = TRUE; + self->locking = FALSE; + self->active_time = g_get_monotonic_time (); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOCKED]); +} + + +static void +phosh_lockscreen_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLockscreenManager *self = PHOSH_LOCKSCREEN_MANAGER (object); + + switch (property_id) { + case PROP_LOCKED: + phosh_lockscreen_manager_set_locked (self, g_value_get_boolean (value)); + break; + case PROP_CALLS_MANAGER: + self->calls_manager = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_lockscreen_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLockscreenManager *self = PHOSH_LOCKSCREEN_MANAGER (object); + + switch (property_id) { + case PROP_LOCKED: + g_value_set_boolean (value, self->locked); + break; + case PROP_CALLS_MANAGER: + g_value_set_object (value, self->calls_manager); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_calls_call_added (PhoshLockscreen *self) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + + g_signal_emit (self, signals[WAKEUP_OUTPUTS], 0); +} + + +static void +phosh_lockscreen_manager_dispose (GObject *object) +{ + PhoshLockscreenManager *self = PHOSH_LOCKSCREEN_MANAGER (object); + + g_clear_pointer (&self->shields, g_ptr_array_unref); + g_clear_pointer (&self->lockscreen, phosh_cp_widget_destroy); + g_clear_object (&self->calls_manager); + + g_cancellable_cancel (self->bg_load_cancel); + g_clear_object (&self->bg_load_cancel); + + g_clear_object (&self->bg_file_monitor); + g_clear_object (&self->bg_file); + g_clear_object (&self->bg_settings); + g_clear_object (&self->cached_bg_image); + + G_OBJECT_CLASS (phosh_lockscreen_manager_parent_class)->dispose (object); +} + + +static void +phosh_lockscreen_manager_constructed (GObject *object) +{ + PhoshLockscreenManager *self = PHOSH_LOCKSCREEN_MANAGER (object); + + G_OBJECT_CLASS (phosh_lockscreen_manager_parent_class)->constructed (object); + + g_signal_connect_object (self->calls_manager, + "call-added", + G_CALLBACK (on_calls_call_added), + self, + G_CONNECT_SWAPPED); +} + + +static void +phosh_lockscreen_manager_class_init (PhoshLockscreenManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_lockscreen_manager_constructed; + object_class->dispose = phosh_lockscreen_manager_dispose; + + object_class->set_property = phosh_lockscreen_manager_set_property; + object_class->get_property = phosh_lockscreen_manager_get_property; + + /** + * PhoshLockscreenManager:locked: + * + * Whether the screen is locked + */ + props[PROP_LOCKED] = + g_param_spec_boolean ("locked", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_CALLS_MANAGER] = + g_param_spec_object ("calls-manager", "", "", + PHOSH_TYPE_CALLS_MANAGER, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshLockscreenManager::wakeup-outputs + * @self: The #PhoshLockscreenManager emitting this signal + * + * Emitted when the outputs should be woken up. + */ + signals[WAKEUP_OUTPUTS] = g_signal_new ( + "wakeup-outputs", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_lockscreen_manager_init (PhoshLockscreenManager *self) +{ + self->bg_settings = g_settings_new (SCREENSAVER_SETTINGS); + + g_object_connect (self->bg_settings, + "swapped-object-signal::changed::" KEY_PICTURE_URI, + G_CALLBACK (on_picture_params_changed), + self, + "swapped-object-signal::changed::" KEY_PICTURE_OPTIONS, + G_CALLBACK (on_picture_params_changed), + self, + NULL); + on_picture_params_changed (self); +} + + +PhoshLockscreenManager * +phosh_lockscreen_manager_new (PhoshCallsManager *calls_manager) +{ + return g_object_new (PHOSH_TYPE_LOCKSCREEN_MANAGER, + "calls-manager", calls_manager, + NULL); +} + +/** + * phosh_lockscreen_set_locked: + * @self: The #PhoshLockscreenManager + * @lock: %TRUE to lock %FALSE to unlock + * + * Lock or unlock the screen. + */ +void +phosh_lockscreen_manager_set_locked (PhoshLockscreenManager *self, gboolean lock) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self)); + if (lock == self->locked) + return; + + if (lock) + lockscreen_lock (self); + else + on_lockscreen_unlock (self, PHOSH_LOCKSCREEN (self->lockscreen)); +} + + +gboolean +phosh_lockscreen_manager_get_locked (PhoshLockscreenManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self), FALSE); + + return self->locked; +} + +/** + * phosh_lockscreen_manager_get_page + * @self: The #PhoshLockscreenManager + * + * Returns: The currently shown #PhoshLockscreenPage in the #PhoshLockscreen + */ +PhoshLockscreenPage +phosh_lockscreen_manager_get_page (PhoshLockscreenManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self), FALSE); + + return phosh_lockscreen_get_page (self->lockscreen); +} + + +gint64 +phosh_lockscreen_manager_get_active_time (PhoshLockscreenManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self), 0); + + return self->active_time; +} + + +gboolean +phosh_lockscreen_manager_set_page (PhoshLockscreenManager *self, + PhoshLockscreenPage page) +{ + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self), FALSE); + + if (!self->lockscreen) + return FALSE; + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN (self->lockscreen), FALSE); + + phosh_lockscreen_set_page (self->lockscreen, page); + return TRUE; +} + + +/** + * phosh_lockscreen_manager_get_lockscreen: + * @self: The lockscreen manager + * + * Gets the current [type@Lockscreen], if one exists (NULL otherwise). + * + * Returns:(transfer none)(nullable): The lockscreen + */ +PhoshLockscreen* +phosh_lockscreen_manager_get_lockscreen (PhoshLockscreenManager *self) +{ + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self), NULL); + + if (!self->lockscreen) + return NULL; + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN (self->lockscreen), FALSE); + + return self->lockscreen; +} diff --git a/src/lockscreen-manager.h b/src/lockscreen-manager.h new file mode 100644 index 000000000..06c9b840f --- /dev/null +++ b/src/lockscreen-manager.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "lockscreen.h" +#include + +#define PHOSH_TYPE_LOCKSCREEN_MANAGER (phosh_lockscreen_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshLockscreenManager, + phosh_lockscreen_manager, + PHOSH, + LOCKSCREEN_MANAGER, + GObject) + +G_BEGIN_DECLS + +void phosh_lockscreen_manager_set_locked (PhoshLockscreenManager *self, + gboolean state); +gboolean phosh_lockscreen_manager_get_locked (PhoshLockscreenManager *self); +gboolean phosh_lockscreen_manager_set_page (PhoshLockscreenManager *self, + PhoshLockscreenPage page); +PhoshLockscreenPage phosh_lockscreen_manager_get_page (PhoshLockscreenManager *self); +gint64 phosh_lockscreen_manager_get_active_time (PhoshLockscreenManager *self); +PhoshLockscreen *phosh_lockscreen_manager_get_lockscreen (PhoshLockscreenManager *self); + +G_END_DECLS diff --git a/src/lockscreen-priv.h b/src/lockscreen-priv.h new file mode 100644 index 000000000..c6ce9da6d --- /dev/null +++ b/src/lockscreen-priv.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "background-image.h" +#include "calls-manager.h" +#include "lockscreen.h" + +G_BEGIN_DECLS + +GtkWidget *phosh_lockscreen_new (GType lockscreen_type, gpointer layer_shell, gpointer wl_output, + PhoshCallsManager *calls_manager); +void phosh_lockscreen_set_bg_image (PhoshLockscreen *self, PhoshBackgroundImage *image); + +G_END_DECLS diff --git a/src/lockscreen.c b/src/lockscreen.c new file mode 100644 index 000000000..065eefa73 --- /dev/null +++ b/src/lockscreen.c @@ -0,0 +1,1428 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2023-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-lockscreen" + +#include "auth.h" +#include "call-notification.h" +#include "calls-manager.h" +#include "keypad.h" +#include "layersurface-priv.h" +#include "lockscreen-bg.h" +#include "lockscreen-priv.h" +#include "notifications/notify-manager.h" +#include "notifications/notification-frame.h" +#include "osk-manager.h" +#include "shell-priv.h" +#include "util.h" +#include "widget-box.h" +#include "wall-clock-priv.h" + +#include "gmobile.h" + +#include +#include +#include + +#include +#include + + +#define LOCKSCREEN_IDLE_SECONDS 5 +#define LOCKSCREEN_SMALL_DATE_AND_TIME_CLASS "p-small" + +#define LOCKSCREEN_SMALL_DISPLAY 700 + +/** + * PhoshLockscreen: + * + * The main lock screen + * + * The lock screen displayed on the primary output featuring the clock + * and unlock keypad. It handles displaying ongoing calls when the + * shell is locked and can be extended via plugins. + * + * Other outputs are locked via PhoshLockshields. + * + * # CSS nodes + * + * `PhoshLockscreen` has a CSS name with the name `phosh-lockscreen`. + */ + + +enum { + PROP_0, + PROP_CALLS_MANAGER, + PROP_PAGE, + PROP_REQUIRE_UNLOCK, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + LOCKSCREEN_UNLOCK, + WAKEUP_OUTPUT, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct { + HdyDeck *deck; + GtkWidget *carousel; + PhoshLockscreenPage default_page; + gboolean require_unlock; + + /* info page */ + GtkWidget *box_info; + GtkWidget *box_datetime; + GtkListBox *list_calls; + GtkWidget *lbl_clock; + GtkWidget *lbl_date; + GtkWidget *list_notifications; + GtkRevealer *rev_call_notifications; + GtkRevealer *rev_media_player; + GtkRevealer *rev_notifications; + GSettings *notification_settings; + guint reveals; + + /* unlock page */ + GtkWidget *box_unlock; + GtkWidget *keypad_revealer; + GtkWidget *keypad; + GtkWidget *entry_pin; + GtkGesture *long_press_del_gesture; + GtkWidget *lbl_unlock_status; + GtkWidget *btn_submit; + GtkWidget *btn_keyboard; + guint idle_timer; + gint64 last_input; + PhoshAuth *auth; + GSettings *lockscreen_settings; + + struct { + GtkGesture *swipe_gesture; + double x_start; + double base; + } brightness; + + /* extra page */ + GtkWidget *extra_page; + + /* widget box */ + PhoshWidgetBox *widget_box; + + /* Call page */ + GtkBox *box_call_display; + CuiCallDisplay *call_display; + + PhoshCallsManager *calls_manager; + char *active; /* opaque handle to the active call */ + + PhoshLockscreenBg *background; +} PhoshLockscreenPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshLockscreen, phosh_lockscreen, PHOSH_TYPE_LAYER_SURFACE) + + +static gboolean +is_valid (PhoshLockscreen *self, GtkGesture *gesture, double *x_center) +{ + double y_center, v_x, v_y; + uint height; + gboolean active; + + active = gtk_gesture_get_bounding_box_center (gesture, x_center, &y_center); + if (!active) + return FALSE; + + height = gtk_widget_get_allocated_height (GTK_WIDGET (self)); + /* Swipe must happen in the upper screen half */ + if (y_center > 0.5 * height) + return FALSE; + + if (!gtk_gesture_swipe_get_velocity (GTK_GESTURE_SWIPE (gesture), &v_x, &v_y)) + return FALSE; + + if (ABS (v_x) > 3.0 * ABS (v_y)) + return TRUE; + + return FALSE; +} + + +static void +on_swipe_gesture_update (PhoshLockscreen *self, + GdkEventSequence *sequence, + GtkGesture *gesture) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshBrightnessManager *manager; + double x_center; + uint width; + double brightness; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + + if (!is_valid (self, gesture, &x_center)) + return; + + manager = phosh_shell_get_brightness_manager (phosh_shell_get_default ()); + width = gtk_widget_get_allocated_width (GTK_WIDGET (self)); + + brightness = priv->brightness.base; + /* Use at least 1% steps to avoid too many updates */ + brightness += 0.01 * (int)(100 * (x_center - priv->brightness.x_start) / width); + + g_debug ("Brightness gesture updating: %f", brightness); + phosh_brightness_manager_set_value (manager, CLAMP (brightness, 0.0, 1.0), TRUE); +} + + +static void +on_swipe_gesture_begin (PhoshLockscreen *self, + GdkEventSequence *sequence, + GtkGesture *gesture) +{ + gdouble x_center; + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshBrightnessManager *manager; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + + if (!is_valid (self, gesture, &x_center)) + return; + + gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED); + manager = phosh_shell_get_brightness_manager (phosh_shell_get_default ()); + priv->brightness.x_start = x_center; + priv->brightness.base = phosh_brightness_manager_get_value (manager); + g_debug ("Brightness gesture start at: %f (%f)", + priv->brightness.x_start, + priv->brightness.base); +} + + +static void +phosh_lockscreen_map (GtkWidget *widget) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (widget); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + GTK_WIDGET_CLASS (phosh_lockscreen_parent_class)->map (widget); + + phosh_layer_surface_set_stacked_below (PHOSH_LAYER_SURFACE (priv->background), + PHOSH_LAYER_SURFACE (self)); +} + + +static void +set_require_unlock (PhoshLockscreen *self, gboolean require_unlock) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + if (priv->require_unlock != require_unlock) { + priv->require_unlock = require_unlock; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REQUIRE_UNLOCK]); + } +} + +static void +phosh_lockscreen_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (object); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + switch (property_id) { + case PROP_CALLS_MANAGER: + priv->calls_manager = g_value_dup_object (value); + break; + case PROP_REQUIRE_UNLOCK: + set_require_unlock (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_lockscreen_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (object); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + switch (property_id) { + case PROP_CALLS_MANAGER: + g_value_set_object (value, priv->calls_manager); + break; + case PROP_PAGE: + g_value_set_enum (value, phosh_lockscreen_get_page (self)); + break; + case PROP_REQUIRE_UNLOCK: + g_value_set_boolean (value, priv->require_unlock); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +clear_input (PhoshLockscreen *self, gboolean clear_all) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + if (clear_all) + gtk_editable_delete_text (GTK_EDITABLE (priv->entry_pin), 0, -1); + else + g_signal_emit_by_name (priv->entry_pin, "backspace", NULL); +} + + +static gboolean +keypad_check_idle (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + gint64 now = g_get_monotonic_time (); + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + if (priv->auth == NULL && now - priv->last_input > LOCKSCREEN_IDLE_SECONDS * 1000 * 1000) { + phosh_lockscreen_set_page (self, priv->default_page); + priv->idle_timer = 0; + return G_SOURCE_REMOVE; + } + return G_SOURCE_CONTINUE; +} + + +static void +show_unlock_page (PhoshLockscreen *self) +{ + phosh_lockscreen_set_page (self, PHOSH_LOCKSCREEN_PAGE_UNLOCK); + /* skip signal on init */ + if (signals[WAKEUP_OUTPUT]) + g_signal_emit (self, signals[WAKEUP_OUTPUT], 0); +} + + +static gboolean +finish_shake_entry (PhoshLockscreen *self) +{ + clear_input (self, TRUE); + gtk_widget_set_sensitive (GTK_WIDGET (self), TRUE); + return G_SOURCE_REMOVE; +} + + +static gboolean +shake_entry (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer data) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (widget); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + gint64 start_time = g_variant_get_int64 (data); + gint64 end_time = start_time + 1000 * 300; + gint64 now = gdk_frame_clock_get_frame_time (frame_clock); + + float t = (float) (now - start_time) / (float) (end_time - start_time); + float pos = sin (t * 10) * 0.05 + 0.5; + + if (now > end_time) { + /* Stop the animation only when we would step over the idle position (0.5) */ + if ((gtk_entry_get_alignment (GTK_ENTRY (priv->entry_pin)) > 0.5 && pos < 0.5) || pos > 0.5) { + guint id; + + gtk_entry_set_alignment (GTK_ENTRY (priv->entry_pin), 0.5); + id = g_timeout_add (400, (GSourceFunc) finish_shake_entry, self); + g_source_set_name_by_id (id, "[PhoshLockscreen] shake PIN entry"); + return FALSE; + } + } + + gtk_entry_set_alignment (GTK_ENTRY (priv->entry_pin), pos); + return TRUE; +} + +static void +focus_pin_entry (PhoshLockscreen *self, gboolean enable_osk) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + if (enable_osk) { + /* restore default OSK behavior */ + g_object_set (priv->entry_pin, "im-module", NULL, NULL); + } + + gtk_widget_set_sensitive (priv->entry_pin, TRUE); + gtk_entry_grab_focus_without_selecting (GTK_ENTRY (priv->entry_pin)); +} + + +static void +on_auth_authenticate_ready (GObject *source_object, GAsyncResult *result, gpointer user_data) +{ + PhoshAuth *auth = PHOSH_AUTH (source_object); + g_autoptr (PhoshLockscreen) self = PHOSH_LOCKSCREEN (user_data); + PhoshLockscreenPrivate *priv; + GError *error = NULL; + gboolean authenticated; + + priv = phosh_lockscreen_get_instance_private (self); + authenticated = phosh_auth_authenticate_finish (auth, result, &error); + if (error != NULL) { + g_warning ("Auth failed unexpected: %s", error->message); + return; + } + + if (authenticated) { + g_signal_emit (self, signals[LOCKSCREEN_UNLOCK], 0); + } else { + /* give visual feedback on error */ + phosh_lockscreen_set_unlock_status (self, _("Enter Passcode")); + phosh_lockscreen_shake_pin_entry (self); + phosh_keypad_distribute (PHOSH_KEYPAD (priv->keypad)); + } + g_clear_object (&priv->auth); + priv->last_input = g_get_monotonic_time (); +} + + +static void +delete_button_clicked_cb (PhoshLockscreen *self, + GtkWidget *widget) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + + clear_input (self, FALSE); +} + +static void +osk_button_clicked_cb (PhoshLockscreen *self, + GtkWidget *widget) +{ + PhoshLockscreenPrivate *priv; + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + priv->last_input = g_get_monotonic_time (); + + focus_pin_entry (self, TRUE); +} + + +static void +on_osk_visibility_changed (PhoshLockscreen *self, + GParamSpec *pspec, + PhoshOskManager *osk) +{ + PhoshLockscreenPrivate *priv; + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + if (!phosh_osk_manager_get_visible (osk)) + g_object_set (priv->entry_pin, "im-module", "gtk-im-context-none", NULL); +} + + +static void +long_press_del_cb (PhoshLockscreen *self, + double x, + double y, + GtkGesture *gesture) +{ + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + g_debug ("Long press on delete button"); + clear_input (self, TRUE); +} + + +static void +input_changed_cb (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + guint16 length; + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + priv->last_input = g_get_monotonic_time (); + + length = gtk_entry_get_text_length (GTK_ENTRY (priv->entry_pin)); + + gtk_widget_set_sensitive (priv->btn_submit, length != 0); +} + + +static void +submit_cb (PhoshLockscreen *self) +{ + PhoshLockscreenClass *klass = PHOSH_LOCKSCREEN_GET_CLASS (self); + + g_return_if_fail (klass->unlock_submit); + klass->unlock_submit (self); +} + + +static gboolean +key_press_event_cb (PhoshLockscreen *self, GdkEventKey *event, gpointer data) +{ + PhoshLockscreenPrivate *priv; + gboolean handled = FALSE; + gboolean on_unlock_page, with_control; + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + on_unlock_page = phosh_lockscreen_get_page (self) == PHOSH_LOCKSCREEN_PAGE_UNLOCK; + with_control = event->state & GDK_CONTROL_MASK; + + if (gtk_entry_im_context_filter_keypress (GTK_ENTRY (priv->entry_pin), event)) { + show_unlock_page (self); + handled = TRUE; + } else { + switch (event->keyval) { + case GDK_KEY_space: + show_unlock_page (self); + handled = TRUE; + break; + case GDK_KEY_Escape: + clear_input (self, TRUE); + phosh_lockscreen_set_page (self, priv->default_page); + handled = TRUE; + break; + case GDK_KEY_Delete: + case GDK_KEY_KP_Delete: + case GDK_KEY_BackSpace: + if (on_unlock_page == TRUE) { + clear_input (self, FALSE); + handled = TRUE; + } + break; + case GDK_KEY_Return: + case GDK_KEY_ISO_Enter: + case GDK_KEY_KP_Enter: + if (on_unlock_page == TRUE) { + submit_cb (self); + handled = TRUE; + } + break; + case GDK_KEY_Left: + if (!on_unlock_page && with_control && hdy_deck_get_can_swipe_back (priv->deck)) { + hdy_deck_navigate (priv->deck, HDY_NAVIGATION_DIRECTION_BACK); + handled = TRUE; + } + break; + case GDK_KEY_Right: + if (!on_unlock_page && with_control && hdy_deck_get_can_swipe_forward (priv->deck)) { + hdy_deck_navigate (priv->deck, HDY_NAVIGATION_DIRECTION_FORWARD); + handled = TRUE; + } + default: + /* nothing to do */ + break; + } + } + + priv->last_input = g_get_monotonic_time (); + return handled; +} + + +static void +wall_clock_notify_cb (PhoshLockscreen *self, + GParamSpec *pspec, + PhoshWallClock *wall_clock) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + const char *time; + g_autofree char *date = NULL; + g_autofree char *stripped = NULL; + + time = phosh_wall_clock_get_clock (wall_clock, TRUE); + + stripped = phosh_wall_clock_strip_am_pm (wall_clock, time); + gtk_label_set_text (GTK_LABEL (priv->lbl_clock), stripped); + + date = phosh_wall_clock_local_date (wall_clock); + gtk_label_set_label (GTK_LABEL (priv->lbl_date), date); +} + + +static void +carousel_position_notified_cb (PhoshLockscreen *self, + GParamSpec *pspec, + HdyCarousel *carousel) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + g_clear_handle_id (&priv->idle_timer, g_source_remove); +} + +static void +carousel_page_changed_cb (PhoshLockscreen *self, + guint index, + HdyCarousel *carousel) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshShell *shell = phosh_shell_get_default (); + PhoshOskManager *osk_manager = phosh_shell_get_osk_manager (shell); + gboolean osk_visible = phosh_osk_manager_get_visible (osk_manager); + PhoshLockscreenPage page = phosh_lockscreen_get_page (self); + + if (page == PHOSH_LOCKSCREEN_PAGE_UNLOCK) { + focus_pin_entry (self, osk_visible); + + if (!priv->idle_timer) { + priv->last_input = g_get_monotonic_time (); + priv->idle_timer = g_timeout_add_seconds (LOCKSCREEN_IDLE_SECONDS, + (GSourceFunc) keypad_check_idle, + self); + g_source_set_name_by_id (priv->idle_timer, "[PhoshLockscreen] keypad check"); + } + if (!priv->require_unlock) { + g_signal_emit (self, signals[LOCKSCREEN_UNLOCK], 0); + return; + } + } else { + gtk_widget_set_sensitive (priv->entry_pin, FALSE); + clear_input (self, TRUE); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]); +} + + +static void +update_active_call (PhoshLockscreen *self, const char *path) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshCall *call; + + g_debug ("New call %s", path); + g_signal_emit (self, signals[WAKEUP_OUTPUT], 0); + + g_free (priv->active); + priv->active = g_strdup (path); + + call = phosh_calls_manager_get_call (priv->calls_manager, path); + g_return_if_fail (PHOSH_IS_CALL (call)); + + cui_call_display_set_call (priv->call_display, CUI_CALL (call)); +} + + +static void +on_calls_call_added (PhoshLockscreen *self, const char *path) +{ + PhoshLockscreenPrivate *priv; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (priv->calls_manager)); + + update_active_call (self, path); + + hdy_deck_set_visible_child (priv->deck, GTK_WIDGET (priv->box_call_display)); +} + + +static void +on_calls_call_removed (PhoshLockscreen *self, const char *path) +{ + PhoshLockscreenPrivate *priv; + + g_return_if_fail (path != NULL); + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + g_debug ("Call %s removed, active: %s", path, priv->active); + + if (g_strcmp0 (path, priv->active)) + return; + + g_clear_pointer (&priv->active, g_free); + + hdy_deck_set_visible_child (priv->deck, GTK_WIDGET (priv->box_info)); +} + + +static GtkWidget * +create_call_notification_row (gpointer item, gpointer data) +{ + PhoshCallNotification *noti = NULL; + PhoshCall *call = PHOSH_CALL (item); + + noti = phosh_call_notification_new (call); + return GTK_WIDGET (noti); +} + + +static void +on_deck_visible_child_changed (PhoshLockscreen *self, GParamSpec *pspec, HdyDeck *deck) +{ + GtkWidget *visible_child; + PhoshLockscreenPrivate *priv; + gboolean swipe_forward = TRUE; + gboolean swipe_back = TRUE; + + g_return_if_fail (HDY_IS_DECK (deck)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + visible_child = hdy_deck_get_visible_child (deck); + + /* Avoid forward swipe to calls page if there's no active call */ + if (visible_child == priv->box_info && + phosh_calls_manager_get_active_call_handle (priv->calls_manager) == NULL) + swipe_forward = FALSE; + + /* Avoid backward swipe to widget-box if there's no plugin */ + if (visible_child == priv->box_info && !phosh_widget_box_has_plugins (priv->widget_box)) + swipe_back = FALSE; + + hdy_deck_set_can_swipe_forward (deck, swipe_forward); + hdy_deck_set_can_swipe_back (deck, swipe_back); +} + + +static void +on_deck_transition_running_changed (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + if (hdy_deck_get_transition_running (priv->deck)) + return; + + /* Otherwise we might see stale information */ + /* See https://gitlab.gnome.org/World/Phosh/phosh/-/issues/922 */ + gtk_widget_queue_draw (priv->lbl_clock); +} + + +static GtkWidget * +create_notification_row (gpointer item, gpointer data) +{ + GtkWidget *row = NULL; + GtkWidget *frame = NULL; + const char *action_filters[] = { "X-Phosh-Lockscreen-Actions", NULL }; + + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "activatable", FALSE, + "visible", TRUE, + NULL); + + frame = phosh_notification_frame_new (FALSE, action_filters); + phosh_notification_frame_bind_model (PHOSH_NOTIFICATION_FRAME (frame), item); + + gtk_widget_set_visible (frame, TRUE); + gtk_container_add (GTK_CONTAINER (row), frame); + + return row; +} + + +static void +on_info_reveal_child_changed (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + gboolean has_info = FALSE; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + has_info |= gtk_revealer_get_reveal_child (priv->rev_notifications); + has_info |= gtk_revealer_get_reveal_child (priv->rev_call_notifications); + has_info |= gtk_revealer_get_reveal_child (priv->rev_media_player); + + /* Use small clock if any additional info elements are revealed */ + phosh_util_toggle_style_class (priv->box_datetime, + LOCKSCREEN_SMALL_DATE_AND_TIME_CLASS, + has_info); +} + + +static void +on_call_notification_activated (PhoshLockscreen *self, + GtkListBoxRow *row, + GtkListBox *list) +{ + PhoshLockscreenPrivate *priv; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + hdy_deck_set_visible_child (priv->deck, GTK_WIDGET (priv->box_call_display)); +} + + + +static void +on_call_notifications_items_changed (PhoshLockscreen *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + PhoshLockscreenPrivate *priv; + gboolean is_empty; + + g_return_if_fail (G_IS_LIST_MODEL (list)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + is_empty = !g_list_model_get_n_items (list); + + gtk_revealer_set_reveal_child (priv->rev_call_notifications, !is_empty); +} + + +static void +on_notification_items_changed (PhoshLockscreen *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + PhoshLockscreenPrivate *priv; + gboolean is_empty; + gboolean show_in_lockscreen; + gboolean reveal; + + g_return_if_fail (G_IS_LIST_MODEL (list)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + show_in_lockscreen = g_settings_get_boolean (priv->notification_settings, "show-in-lock-screen"); + is_empty = !g_list_model_get_n_items (list); + + reveal = show_in_lockscreen && !is_empty; + + gtk_revealer_set_reveal_child (priv->rev_notifications, reveal); +} + + +static void +on_show (PhoshLockscreen *self, gpointer userdata) +{ + PhoshLockscreenPrivate *priv; + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + phosh_lockscreen_set_page (self, priv->default_page); +} + + +static void +phosh_lockscreen_add_background (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshWayland *wl = phosh_wayland_get_default (); + struct wl_output *wl_output; + + wl_output = phosh_layer_surface_get_wl_output (PHOSH_LAYER_SURFACE (self)); + priv->background = phosh_lockscreen_bg_new (phosh_wayland_get_zwlr_layer_shell_v1 (wl), + wl_output); + g_object_bind_property (self, "visible", priv->background, "visible", G_BINDING_SYNC_CREATE); +} + + +static void +phosh_lockscreen_constructed (GObject *object) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (object); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + PhoshWallClock *wall_clock = phosh_wall_clock_get_default (); + const char *active; + PhoshNotifyManager *manager; + PhoshOskManager *osk_manager; + PhoshShell *shell; + g_autoptr (GSettings) plugin_settings = NULL; + g_auto (GStrv) plugins = NULL; + + G_OBJECT_CLASS (phosh_lockscreen_parent_class)->constructed (object); + + /* window properties */ + gtk_window_set_title (GTK_WINDOW (self), "phosh lockscreen"); + gtk_window_set_decorated (GTK_WINDOW (self), FALSE); + + gtk_widget_add_events (GTK_WIDGET (self), GDK_KEY_PRESS_MASK); + g_signal_connect (G_OBJECT (self), + "key_press_event", + G_CALLBACK (key_press_event_cb), + NULL); + g_signal_connect (G_OBJECT (self), + "show", + G_CALLBACK (on_show), + NULL); + + g_signal_connect_object (wall_clock, + "notify::time", + G_CALLBACK (wall_clock_notify_cb), + self, + G_CONNECT_SWAPPED); + wall_clock_notify_cb (self, NULL, wall_clock); + + g_signal_connect_object (priv->calls_manager, + "call-added", + G_CALLBACK (on_calls_call_added), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (priv->calls_manager, + "call-removed", + G_CALLBACK (on_calls_call_removed), + self, + G_CONNECT_SWAPPED); + + gtk_list_box_bind_model (GTK_LIST_BOX (priv->list_calls), + G_LIST_MODEL (priv->calls_manager), + create_call_notification_row, + NULL, + NULL); + g_signal_connect_object (G_LIST_MODEL (priv->calls_manager), + "items-changed", + G_CALLBACK (on_call_notifications_items_changed), + self, + G_CONNECT_SWAPPED); + on_call_notifications_items_changed (self, -1, -1, -1, G_LIST_MODEL (priv->calls_manager)); + active = phosh_calls_manager_get_active_call_handle (priv->calls_manager); + if (active) + update_active_call (self, active); + + manager = phosh_notify_manager_get_default (); + priv->notification_settings = g_settings_new (PHOSH_NOTIFICATIONS_SCHEMA_ID); + gtk_list_box_bind_model (GTK_LIST_BOX (priv->list_notifications), + G_LIST_MODEL (phosh_notify_manager_get_list (manager)), + create_notification_row, + NULL, + NULL); + g_signal_connect_object (phosh_notify_manager_get_list (manager), + "items-changed", + G_CALLBACK (on_notification_items_changed), + self, + G_CONNECT_SWAPPED); + on_notification_items_changed (self, -1, -1, -1, + G_LIST_MODEL (phosh_notify_manager_get_list (manager))); + + shell = phosh_shell_get_default (); + osk_manager = phosh_shell_get_osk_manager (shell); + g_object_bind_property (osk_manager, "visible", + priv->keypad_revealer, "reveal-child", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + g_signal_connect_object (osk_manager, "notify::visible", + G_CALLBACK (on_osk_visibility_changed), self, + G_CONNECT_SWAPPED); + g_object_bind_property (osk_manager, "available", + priv->btn_keyboard, "sensitive", + G_BINDING_SYNC_CREATE); + + priv->lockscreen_settings = g_settings_new ("sm.puri.phosh.lockscreen"); + g_settings_bind (priv->lockscreen_settings, "shuffle-keypad", + priv->keypad, "shuffle", + G_SETTINGS_BIND_GET); + g_settings_bind (priv->lockscreen_settings, "require-unlock", + self, "require-unlock", + G_SETTINGS_BIND_GET); + + g_object_bind_property (self, "require-unlock", + priv->box_unlock, "visible", + G_BINDING_SYNC_CREATE); + + plugin_settings = g_settings_new ("sm.puri.phosh.plugins"); + plugins = g_settings_get_strv (plugin_settings, "lock-screen"); + if (plugins) + phosh_widget_box_set_plugins (PHOSH_WIDGET_BOX (priv->widget_box), plugins); + + on_deck_visible_child_changed (self, NULL, priv->deck); + on_info_reveal_child_changed (self); + + phosh_lockscreen_add_background (self); +} + +static void +deck_back_clicked_cb (GtkWidget *sender, + PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + hdy_deck_set_visible_child (priv->deck, GTK_WIDGET (priv->box_info)); +} + + +static void +deck_forward_clicked_cb (GtkWidget *sender, + PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + hdy_deck_set_visible_child (priv->deck, GTK_WIDGET (priv->box_info)); +} + + +static void +phosh_lockscreen_dispose (GObject *object) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (object); + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + g_clear_object (&priv->notification_settings); + g_clear_handle_id (&priv->idle_timer, g_source_remove); + g_clear_object (&priv->calls_manager); + g_clear_pointer (&priv->active, g_free); + g_clear_object (&priv->lockscreen_settings); + + g_clear_pointer (&priv->background, phosh_cp_widget_destroy); + + G_OBJECT_CLASS (phosh_lockscreen_parent_class)->dispose (object); +} + + +static void +phosh_lockscreen_configured (PhoshLayerSurface *layer_surface) +{ + PhoshLockscreen *self = PHOSH_LOCKSCREEN (layer_surface); + PhoshLockscreenPrivate *priv; + guint height, margin = 100; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + + priv = phosh_lockscreen_get_instance_private (self); + height = phosh_layer_surface_get_configured_height (layer_surface); + + /* Avoid margin on smaller displays */ + if (height < LOCKSCREEN_SMALL_DISPLAY) + margin = 0; + + gtk_widget_set_margin_top (GTK_WIDGET (priv->box_unlock), margin); + + PHOSH_LAYER_SURFACE_CLASS (phosh_lockscreen_parent_class)->configured (layer_surface); +} + + +static void +on_unlock_submit (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + const char *input; + guint16 length; + + g_assert (PHOSH_IS_LOCKSCREEN (self)); + + priv = phosh_lockscreen_get_instance_private (self); + priv->last_input = g_get_monotonic_time (); + + length = gtk_entry_get_text_length (GTK_ENTRY (priv->entry_pin)); + if (length == 0) + return; + + input = gtk_entry_get_text (GTK_ENTRY (priv->entry_pin)); + + phosh_lockscreen_set_unlock_status (self, _("Checking…")); + gtk_widget_set_sensitive (GTK_WIDGET (self), FALSE); + + if (priv->auth == NULL) + priv->auth = PHOSH_AUTH (phosh_auth_new ()); + phosh_auth_authenticate_async (priv->auth, + input, + NULL, + on_auth_authenticate_ready, + g_object_ref (self)); +} + + +static void +phosh_lockscreen_class_init (PhoshLockscreenClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + + object_class->constructed = phosh_lockscreen_constructed; + object_class->dispose = phosh_lockscreen_dispose; + + object_class->set_property = phosh_lockscreen_set_property; + object_class->get_property = phosh_lockscreen_get_property; + + widget_class->map = phosh_lockscreen_map; + + layer_surface_class->configured = phosh_lockscreen_configured; + + klass->unlock_submit = on_unlock_submit; + + /** + * PhoshLockscreen:calls-manager: + * + * The calls manager handling incoming and active calls. + */ + props[PROP_CALLS_MANAGER] = + g_param_spec_object ("calls-manager", "", "", + PHOSH_TYPE_CALLS_MANAGER, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshLockscreen:require-unlock: + * + * Require entering PIN or password to unlock. If false, unlock by swiping up. + */ + props[PROP_REQUIRE_UNLOCK] = + g_param_spec_boolean ("require-unlock", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshLockscreen:page: + * + * The currently active carousel page + */ + props[PROP_PAGE] = + g_param_spec_enum ("page", "", "", + PHOSH_TYPE_LOCKSCREEN_PAGE, + PHOSH_LOCKSCREEN_PAGE_UNLOCK, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshLockscreen::lockscreen-unlock + * @self: The #PhoshLockscreen emitting this signal + * + * This signal is emitted when authentication was successful and the + * session should be unlocked. + */ + signals[LOCKSCREEN_UNLOCK] = g_signal_new ("lockscreen-unlock", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + /** + * PhoshLockscreen::wakeup-output + * @self: The #PhoshLockscreen emitting this signal + * + * Emitted when the output showing the lock screen should be woken + * up. + */ + signals[WAKEUP_OUTPUT] = g_signal_new ("wakeup-output", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + g_type_ensure (PHOSH_TYPE_KEYPAD); + g_type_ensure (PHOSH_TYPE_WIDGET_BOX); + gtk_widget_class_set_css_name (widget_class, "phosh-lockscreen"); + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/lockscreen.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, carousel); + gtk_widget_class_bind_template_callback_full (widget_class, + "carousel_position_notified_cb", + G_CALLBACK (carousel_position_notified_cb)); + gtk_widget_class_bind_template_callback_full (widget_class, + "carousel_page_changed_cb", + G_CALLBACK (carousel_page_changed_cb)); + gtk_widget_class_bind_template_child_full (widget_class, + "swipe_gesture", + FALSE, + G_PRIVATE_OFFSET (PhoshLockscreen, + brightness.swipe_gesture)); + + gtk_widget_class_bind_template_callback (widget_class, on_swipe_gesture_begin); + gtk_widget_class_bind_template_callback (widget_class, on_swipe_gesture_update); + + /* main deck */ + gtk_widget_class_bind_template_callback (widget_class, deck_forward_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, deck_forward_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, deck_back_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, on_deck_visible_child_changed); + gtk_widget_class_bind_template_callback (widget_class, on_deck_transition_running_changed); + + /* unlock page */ + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, box_unlock); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, keypad); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, keypad_revealer); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, entry_pin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, lbl_unlock_status); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, + long_press_del_gesture); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, btn_submit); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, btn_keyboard); + + gtk_widget_class_bind_template_callback (widget_class, delete_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, input_changed_cb); + gtk_widget_class_bind_template_callback (widget_class, long_press_del_cb); + gtk_widget_class_bind_template_callback (widget_class, osk_button_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, submit_cb); + + /* info page */ + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, box_info); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, box_datetime); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, lbl_clock); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, lbl_date); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, list_calls); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, list_notifications); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, + rev_call_notifications); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, rev_media_player); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, rev_notifications); + gtk_widget_class_bind_template_callback (widget_class, on_call_notification_activated); + gtk_widget_class_bind_template_callback (widget_class, on_info_reveal_child_changed); + gtk_widget_class_bind_template_callback (widget_class, show_unlock_page); + + /* plugin page */ + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, widget_box); + + /* Call UI */ + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, deck); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, box_call_display); + gtk_widget_class_bind_template_child_private (widget_class, PhoshLockscreen, call_display); +} + + +static void +phosh_lockscreen_init (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + /* LTR doesn't work for the deck + * https://gitlab.gnome.org/World/Phosh/phosh/-/issues/1132 */ + gtk_widget_set_direction (GTK_WIDGET (priv->deck), GTK_TEXT_DIR_LTR); +} + + +GtkWidget * +phosh_lockscreen_new (GType lockscreen_type, + gpointer layer_shell, + gpointer wl_output, + PhoshCallsManager *calls_manager) +{ + g_assert (g_type_is_a (lockscreen_type, phosh_lockscreen_get_type ())); + return g_object_new (lockscreen_type, + "layer-shell", layer_shell, + "wl-output", wl_output, + "anchor", (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT), + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", TRUE, + "exclusive-zone", -1, + "namespace", "phosh lockscreen", + "calls-manager", calls_manager, + NULL); +} + +/** + * phosh_lockscreen_get_page: + * @self: The `PhoshLockscreen` + * + * Returns: The #PhoshLockscreenPage that is currently shown + */ +PhoshLockscreenPage +phosh_lockscreen_get_page (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + guint position; + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN (self), PHOSH_LOCKSCREEN_PAGE_INFO); + priv = phosh_lockscreen_get_instance_private (self); + + /* Round to nearest page - the "current" page is a somewhat arbitrary concept if the carousel + * is animating (or being manually swiped) from one page to another. By rounding like this, the + * key_press_event_cb starts accepting input as soon as at least half the unlock page has swiped + * in. */ + position = round (hdy_carousel_get_position (HDY_CAROUSEL (priv->carousel))); + + if (position == 0) + return PHOSH_LOCKSCREEN_PAGE_INFO; + + if (position == 1 && priv->extra_page) + return PHOSH_LOCKSCREEN_PAGE_EXTRA; + + return PHOSH_LOCKSCREEN_PAGE_UNLOCK; +} + +/** + * phosh_lockscreen_set_page: + * @self: The `PhoshLockscreen` + * page: The page to scroll to + * + * Scrolls to a specific page in the carousel. The state of the deck + * isn't changed. + */ +void +phosh_lockscreen_set_page (PhoshLockscreen *self, PhoshLockscreenPage page) +{ + GtkWidget *scroll_to; + PhoshLockscreenPrivate *priv; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + switch (page) { + case PHOSH_LOCKSCREEN_PAGE_EXTRA: + scroll_to = priv->extra_page; + if (scroll_to) + break; + /* there's no extra page set, so ... */ + G_GNUC_FALLTHROUGH; + case PHOSH_LOCKSCREEN_PAGE_INFO: + scroll_to = GTK_WIDGET (priv->deck); + break; + case PHOSH_LOCKSCREEN_PAGE_UNLOCK: + scroll_to = priv->box_unlock; + break; + default: + scroll_to = GTK_WIDGET (priv->deck); + break; + } + + hdy_carousel_scroll_to (HDY_CAROUSEL (priv->carousel), scroll_to); +} + +/** + * phosh_lockscreen_set_default_page: + * @self: The `PhoshLockscreen` + * page: the page to show by default + * + * Specifies which page should be shown by default when the lockscreen is made visible. This will + * also be the page that is shown when the keypad idle timer is reached. + */ +void +phosh_lockscreen_set_default_page (PhoshLockscreen *self, PhoshLockscreenPage page) +{ + PhoshLockscreenPrivate *priv; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + priv->default_page = page; +} + +/** + * phosh_lockscreen_get_pin_entry: + * @self: The `PhoshLockscreen` + * + * Get the current contents of the keypad PIN entry buffer + * + * Returns: the contents of the entry buffer + */ +const char* +phosh_lockscreen_get_pin_entry (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN (self), ""); + priv = phosh_lockscreen_get_instance_private (self); + return gtk_entry_get_text (GTK_ENTRY (priv->entry_pin)); +} + +/** + * phosh_lockscreen_clear_pin_entry: + * @self: The `PhoshLockscreen` + * + * Clears the current contents of the keypad PIN entry buffer + */ +void +phosh_lockscreen_clear_pin_entry (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + gtk_editable_delete_text (GTK_EDITABLE (priv->entry_pin), 0, -1); +} + +/** + * phosh_lockscreen_shake_pin_entry: + * @self: The `PhoshLockscreen` + * + * Triggers an animation that shakes the PIN entry left and right for a brief period. + * After the animation is complete, the PIN entry buffer is cleared. Used to visually indicate + * authentication errors. + */ +void +phosh_lockscreen_shake_pin_entry (PhoshLockscreen *self) +{ + PhoshLockscreenPrivate *priv; + GdkFrameClock *clock; + gint64 now; + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + clock = gtk_widget_get_frame_clock (priv->entry_pin); + now = gdk_frame_clock_get_frame_time (clock); + gtk_widget_add_tick_callback (GTK_WIDGET (self), + shake_entry, + g_variant_ref_sink (g_variant_new_int64 (now)), + (GDestroyNotify) g_variant_unref); +} + +/** + * phosh_lockscreen_add_extra_page: + * @self: The `PhoshLockscreen` + * @widget: The extra #GtkWidget to insert into the lockscreen carousel + * + * Inserts a custom widget into the "extra" page of the lockscreen. This page sits in-between the + * info page and the keypad page. By default, this page does not exist and is not used. Once an + * extra page is added, it can be navigated to by swiping and also via calls to + * [method@Lockscreen.set_default_page]. + */ +void +phosh_lockscreen_add_extra_page (PhoshLockscreen *self, GtkWidget *widget) +{ + PhoshLockscreenPrivate *priv; + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + priv->extra_page = widget; + hdy_carousel_insert (HDY_CAROUSEL (priv->carousel), priv->extra_page, 1); +} + +/** + * phosh_lockscreen_set_unlock_status: + * @self: The `PhoshLockscreen` + * @status: The status text + * + * Sets the text displayed in the unlock status label. + */ +void +phosh_lockscreen_set_unlock_status (PhoshLockscreen *self, const char *status) +{ + PhoshLockscreenPrivate *priv; + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + priv = phosh_lockscreen_get_instance_private (self); + + gtk_label_set_label (GTK_LABEL (priv->lbl_unlock_status), status); +} + +/** + * phosh_lockscreen_set_bg_image: + * @self: The lockscrenn + * @image: The background image to set + */ +void +phosh_lockscreen_set_bg_image (PhoshLockscreen *self, PhoshBackgroundImage *image) +{ + PhoshLockscreenPrivate *priv = phosh_lockscreen_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_LOCKSCREEN (self)); + g_return_if_fail (image == NULL || PHOSH_IS_BACKGROUND_IMAGE (image)); + + phosh_lockscreen_bg_set_image (priv->background, image); +} diff --git a/src/lockscreen.h b/src/lockscreen.h new file mode 100644 index 000000000..baac5b313 --- /dev/null +++ b/src/lockscreen.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "layersurface.h" + +G_BEGIN_DECLS + +/** + * PhoshLockscreenPage: + * @PHOSH_LOCKSCREEN_PAGE_INFO: The info page (clock, notifications, MPRIS, etc) + * @PHOSH_LOCKSCREEN_PAGE_EXTRA: The extra page (an extension point used by Lockscreen subclasses) + * @PHOSH_LOCKSCREEN_PAGE_UNLOCK: The unlock page (where PIN is entered) + * + * Indicates which page is currently shown on the lockscreen. + * + * This helps `PhoshGnomeShellManager` to decide when to emit + * AcceleratorActivated events over DBus + */ +typedef enum { + PHOSH_LOCKSCREEN_PAGE_INFO, + PHOSH_LOCKSCREEN_PAGE_EXTRA, + PHOSH_LOCKSCREEN_PAGE_UNLOCK, +} PhoshLockscreenPage; + +#define PHOSH_TYPE_LOCKSCREEN (phosh_lockscreen_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshLockscreen, phosh_lockscreen, PHOSH, LOCKSCREEN, PhoshLayerSurface) + +/** + * PhoshLockscreenClass: + * @parent_class: The parent class + * @unlock_submit: This function is invoked when a PIN or password is submitted from the lockscreen + * keypad. It allows to implement a custom authentication mechanism. To indicate success the + * `lockscreen-unlock` signal should be emitted. + */ +struct _PhoshLockscreenClass { + PhoshLayerSurfaceClass parent_class; + void (*unlock_submit) (PhoshLockscreen *self); + + /* Padding for future expansion */ + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + + +void phosh_lockscreen_set_page (PhoshLockscreen *self, + PhoshLockscreenPage page); +PhoshLockscreenPage phosh_lockscreen_get_page (PhoshLockscreen *self); +void phosh_lockscreen_set_default_page (PhoshLockscreen *self, + PhoshLockscreenPage page); + +const char * phosh_lockscreen_get_pin_entry (PhoshLockscreen *self); +void phosh_lockscreen_clear_pin_entry (PhoshLockscreen *self); +void phosh_lockscreen_shake_pin_entry (PhoshLockscreen *self); + +void phosh_lockscreen_set_unlock_status (PhoshLockscreen *self, const char *status); + +void phosh_lockscreen_add_extra_page (PhoshLockscreen *self, GtkWidget *widget); + +G_END_DECLS diff --git a/src/lockshield.c b/src/lockshield.c new file mode 100644 index 000000000..edc7a2a57 --- /dev/null +++ b/src/lockshield.c @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-lockshield" + +#include "phosh-config.h" + +#include "lockshield.h" + +/** + * PhoshLockshield: + * + * Lock shield for non primary screens + * + * The #PhoshLockshield is displayed on lock screens + * which are not the primary one. + */ +struct _PhoshLockshield +{ + PhoshLayerSurface parent; +}; + +G_DEFINE_TYPE(PhoshLockshield, phosh_lockshield, PHOSH_TYPE_LAYER_SURFACE) + + +static void +phosh_lockshield_constructed (GObject *object) +{ + PhoshLockshield *self = PHOSH_LOCKSHIELD (object); + + G_OBJECT_CLASS (phosh_lockshield_parent_class)->constructed (object); + + gtk_style_context_add_class ( + gtk_widget_get_style_context (GTK_WIDGET (self)), + "phosh-lockshield"); +} + + +static void +phosh_lockshield_class_init (PhoshLockshieldClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + + object_class->constructed = phosh_lockshield_constructed; +} + + +static void +phosh_lockshield_init (PhoshLockshield *self) +{ +} + + +GtkWidget * +phosh_lockshield_new (struct zwlr_layer_shell_v1 *layer_shell, + PhoshMonitor *monitor) +{ + return g_object_new (PHOSH_TYPE_LOCKSHIELD, + "layer-shell", layer_shell, + "wl-output", monitor->wl_output, + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", FALSE, + "exclusive-zone", -1, + "namespace", "phosh lockshield", + NULL); +} diff --git a/src/lockshield.h b/src/lockshield.h new file mode 100644 index 000000000..be3845e23 --- /dev/null +++ b/src/lockshield.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include +#include "monitor/monitor.h" +#include "layersurface.h" + +#define PHOSH_TYPE_LOCKSHIELD (phosh_lockshield_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshLockshield, phosh_lockshield, PHOSH, LOCKSHIELD, PhoshLayerSurface) + +GtkWidget *phosh_lockshield_new (struct zwlr_layer_shell_v1 *layer_shell, + PhoshMonitor *monitor); diff --git a/src/main.c b/src/main.c new file mode 100644 index 000000000..f3b6fc05a --- /dev/null +++ b/src/main.c @@ -0,0 +1,163 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-main" + +#include "phosh-config.h" + +#include "audio-manager.h" +#include "shell-priv.h" +#include "phosh-wayland.h" +#include "wall-clock.h" +#include "fake-clock.h" +#include "background-cache.h" +#include "metainfo-cache.h" + +#include +#include + +#include +#include + +#include + +static gboolean +quit (gpointer unused) +{ + g_debug ("Cleaning up"); + gtk_main_quit (); + + return G_SOURCE_REMOVE; +} + + +static gboolean +on_shutdown_signal (gpointer unused) +{ + guint id; + + phosh_shell_fade_out (phosh_shell_get_default (), 0); + id = g_timeout_add_seconds (2, (GSourceFunc)quit, NULL); + g_source_set_name_by_id (id, "[PhoshMain] quit"); + + return FALSE; +} + + +G_NORETURN static void +print_version (void) +{ + printf ("Phosh %s - A Wayland shell for mobile devices\n", PHOSH_VERSION); + exit (0); +} + + +static void +on_shell_ready (PhoshShell *shell, GTimer *timer) +{ + g_timer_stop (timer); + g_message ("Phosh ready after %.2fs", g_timer_elapsed (timer, NULL)); + + /* Inform systemd we're up */ + sd_notify (0, "READY=1"); + + g_signal_handlers_disconnect_by_data (shell, timer); +} + + +static PhoshWallClock * +get_clock (void) +{ + PhoshWallClock *wall_clock = NULL; + g_autoptr (GDateTime) dt = NULL; + const char *clock_str = g_getenv ("PHOSH_FAKE_CLOCK"); + + if (clock_str) { + if (g_str_equal (clock_str, "now")) { + dt = g_date_time_new_now_local (); + } else { + dt = g_date_time_new_from_iso8601 (clock_str, NULL); + if (!dt) + g_critical ("Failed to create fake clock from '%s'", clock_str); + } + } + + if (dt) + wall_clock = PHOSH_WALL_CLOCK (phosh_fake_clock_new (dt)); + + if (!wall_clock) + wall_clock = phosh_wall_clock_new (); + + return wall_clock; +} + + +int +main (int argc, char *argv[]) +{ + g_autoptr (GOptionContext) opt_context = NULL; + g_autoptr (GError) err = NULL; + gboolean unlocked = FALSE, locked = FALSE, version = FALSE; + g_autoptr (PhoshWayland) wl = NULL; + g_autoptr (PhoshShell) shell = NULL; + g_autoptr (PhoshBackgroundCache) background_cache = NULL; + g_autoptr (PhoshMetainfoCache) metainfo_cache = NULL; + g_autoptr (GTimer) timer = g_timer_new (); + g_autoptr (PhoshWallClock) wall_clock = NULL; + g_autoptr (PhoshAudioManager) audio_manager = NULL; + + const GOptionEntry options [] = { + {"unlocked", 'U', 0, G_OPTION_ARG_NONE, &unlocked, + "Don't start with screen locked", NULL}, + {"locked", 'L', 0, G_OPTION_ARG_NONE, &locked, + "Start with screen locked, no matter what", NULL}, + {"version", 0, 0, G_OPTION_ARG_NONE, &version, + "Show version information", NULL}, + { NULL, 0, 0, G_OPTION_ARG_NONE, NULL, NULL, NULL } + }; + + opt_context = g_option_context_new ("- A phone graphical shell"); + g_option_context_add_main_entries (opt_context, options, NULL); + g_option_context_add_group (opt_context, gtk_get_option_group (FALSE)); + if (!g_option_context_parse (opt_context, &argc, &argv, &err)) { + g_warning ("%s", err->message); + return 1; + } + + if (version) + print_version (); + + textdomain (GETTEXT_PACKAGE); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + gtk_init (&argc, &argv); + hdy_init (); + lfb_init (PHOSH_APP_ID, NULL); + + g_unix_signal_add (SIGTERM, on_shutdown_signal, NULL); + g_unix_signal_add (SIGINT, on_shutdown_signal, NULL); + + wall_clock = get_clock (); + phosh_wall_clock_set_default (wall_clock); + wl = phosh_wayland_get_default (); + background_cache = phosh_background_cache_get_default (); + metainfo_cache = phosh_metainfo_cache_get_default (); + audio_manager = phosh_audio_manager_get_default (); + shell = phosh_shell_new (); + phosh_shell_set_default (shell); + + g_signal_connect (shell, "ready", G_CALLBACK (on_shell_ready), timer); + + if (!(unlocked || phosh_shell_started_by_display_manager (shell)) || locked) + phosh_shell_lock (shell); + + gtk_main (); + + return EXIT_SUCCESS; +} diff --git a/src/manager.c b/src/manager.c new file mode 100644 index 000000000..5c8100b9f --- /dev/null +++ b/src/manager.c @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-manager" + +#include "phosh-config.h" + +#include "manager.h" + +/** + * PhoshManager: + * + * Base class for manager objects + * + * Common functionality for manager objects. + */ + +typedef struct +{ + guint idle_id; +} PhoshManagerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshManager, phosh_manager, G_TYPE_OBJECT); + + +static void +on_idle (gpointer user_data) +{ + PhoshManager *self = PHOSH_MANAGER (user_data); + PhoshManagerClass *klass = PHOSH_MANAGER_GET_CLASS (self); + PhoshManagerPrivate *priv = phosh_manager_get_instance_private (self); + + if (klass->idle_init) + (*klass->idle_init) (self); + + priv->idle_id = 0; +} + + +static void +phosh_manager_dispose (GObject *object) +{ + PhoshManager *self = PHOSH_MANAGER (object); + PhoshManagerPrivate *priv = phosh_manager_get_instance_private (self); + + g_clear_handle_id (&priv->idle_id, g_source_remove); + + G_OBJECT_CLASS (phosh_manager_parent_class)->dispose (object); +} + + +static void +phosh_manager_constructed (GObject *object) +{ + PhoshManager *self = PHOSH_MANAGER (object); + PhoshManagerClass *klass = PHOSH_MANAGER_GET_CLASS (object); + PhoshManagerPrivate *priv = phosh_manager_get_instance_private (self); + + G_OBJECT_CLASS (phosh_manager_parent_class)->constructed (object); + + if (klass->idle_init) { + priv->idle_id = g_idle_add_once (on_idle, self); + g_source_set_name_by_id (priv->idle_id, "[PhoshManager] idle"); + } +} + + +static void +phosh_manager_class_init (PhoshManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_manager_constructed; + object_class->dispose = phosh_manager_dispose; +} + +static void +phosh_manager_init (PhoshManager *self) +{ +} diff --git a/src/manager.h b/src/manager.h new file mode 100644 index 000000000..9272218fc --- /dev/null +++ b/src/manager.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MANAGER (phosh_manager_get_type()) + +G_DECLARE_DERIVABLE_TYPE (PhoshManager, phosh_manager, PHOSH, MANAGER, GObject) + +/** + * PhoshManagerClass: + * @parent_class: The parent class + * @idle_init: a callback to be invoked once on idle + */ +struct _PhoshManagerClass +{ + GObjectClass parent_class; + + void (*idle_init) (PhoshManager *self); +}; + +G_END_DECLS diff --git a/src/media-player.c b/src/media-player.c new file mode 100644 index 000000000..1977640fa --- /dev/null +++ b/src/media-player.c @@ -0,0 +1,1044 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2024-2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-media-player" + +#include "phosh-config.h" + +#include "mpris-dbus.h" +#include "mpris-manager.h" +#include "media-player.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include + +#include + +#include + +#define ART_PIXEL_SIZE 48 +#define SEEK_SECOND 1000000 +#define SEEK_BACK (-10 * SEEK_SECOND) +#define SEEK_FORWARD (30 * SEEK_SECOND) + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_t, cairo_destroy) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (cairo_surface_t, cairo_surface_destroy) + +/** + * PhoshMediaPlayer: + * + * A simple MPRIS media player widget + * + * The #PhoshMediaPlayer widget uses `PhoshMprisManager` to + * interface with media players allowing to skip through music and + * raising the player. + * + * Whenever valid mpris player is set, the #PhoshMediaPlayer:attached + * property is set to %TRUE. This can e.g. be used with + * #g_object_bind_property() to toggle the widget's visibility. + * + * # Example + * + * |[ + * + * + * + * ]| + */ + +enum { + PROP_0, + PROP_ATTACHED, + PROP_PLAYABLE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + PLAYER_RAISED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +typedef struct _PhoshMediaPlayerPrivate { + GtkWidget *btn_play; + GtkWidget *btn_next; + GtkWidget *btn_prev; + GtkWidget *btn_details; + GtkWidget *btn_seek_backward; + GtkWidget *btn_seek_forward; + GtkWidget *img_art; + GtkWidget *img_play; + GtkWidget *lbl_title; + GtkWidget *lbl_artist; + GtkWidget *box_pos_len; + GtkWidget *prb_position; + GtkWidget *lbl_position; + GtkWidget *lbl_length; + + char *url; + GCancellable *cancel; + GCancellable *fetch_icon_cancel; + PhoshMprisManager *manager; + /* Actual player controls */ + PhoshDBusMediaPlayer2Player *player; + PhoshMediaPlayerStatus status; + gboolean attached; + gboolean playable; + gint64 track_length; + gint64 track_position; + guint pos_poller_id; +} PhoshMediaPlayerPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshMediaPlayer, phosh_media_player, GTK_TYPE_GRID); + + +static void +phosh_media_player_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshMediaPlayer *self = PHOSH_MEDIA_PLAYER (object); + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + switch (property_id) { + case PROP_ATTACHED: + g_value_set_boolean (value, priv->attached); + break; + case PROP_PLAYABLE: + g_value_set_boolean (value, priv->playable); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +set_playable (PhoshMediaPlayer *self, gboolean playable) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + if (priv->playable == playable) + return; + + priv->playable = playable; + g_debug ("Playable: %d", playable); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PLAYABLE]); +} + + +static void +set_attached (PhoshMediaPlayer *self, gboolean attached) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + + if (priv->attached == attached) + return; + + priv->attached = attached; + if (!attached) + set_playable (self, FALSE); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ATTACHED]); +} + + +static void +update_position (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autofree char *position_text = NULL; + double level = 0.0; + + if (priv->track_position >= 0) + position_text = cui_call_format_duration ((double) priv->track_position / G_USEC_PER_SEC); + + gtk_label_set_label (GTK_LABEL (priv->lbl_position), position_text ?: "-"); + + if (priv->track_position > 0 && priv->track_length > 0) + level = ((double) priv->track_position) / priv->track_length; + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->prb_position), level); +} + + +static void +stop_pos_poller (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + if (priv->pos_poller_id <= 0) + return; + + g_debug ("Stopping position poller"); + g_source_remove (priv->pos_poller_id); + priv->pos_poller_id = 0; +} + + +static void +on_poll_position_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GDBusProxy *proxy = G_DBUS_PROXY (source_object); + PhoshMediaPlayer *self; + PhoshMediaPlayerPrivate *priv; + g_autoptr (GError) err = NULL; + g_autoptr (GVariant) var = NULL; + + var = g_dbus_proxy_call_finish (proxy, res, &err); + if (!var && g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (user_data)); + self = PHOSH_MEDIA_PLAYER (user_data); + priv = phosh_media_player_get_instance_private (self); + + if (var) { + g_autoptr (GVariant) var2 = NULL; + + /* Return variant has type "(v)" where v has type x (i.e. gint64) */ + g_variant_get_child (var, 0, "v", &var2); + priv->track_position = g_variant_get_int64 (var2); + g_debug ("MPRIS Position: %" G_GINT64_FORMAT, priv->track_position); + } else { + g_warning ("Could not get Position from MPRIS player, hiding box_pos_len: %s", err->message); + priv->track_position = -1; + gtk_widget_set_visible (priv->box_pos_len, FALSE); + stop_pos_poller (self); + } + + update_position (self); +} + + +static gboolean +poll_position (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MEDIA_PLAYER (self), G_SOURCE_CONTINUE); + + if (!priv->attached || priv->player == NULL) { + g_debug ("No MPRIS player attached"); + priv->pos_poller_id = 0; + return G_SOURCE_REMOVE; + } + + if (!gtk_widget_is_visible (priv->lbl_position)) { + g_debug ("Widget hidden, not updating Position"); + return G_SOURCE_CONTINUE; + } + + g_dbus_proxy_call (G_DBUS_PROXY (priv->player), + "org.freedesktop.DBus.Properties.Get", + g_variant_new ("(ss)", "org.mpris.MediaPlayer2.Player", "Position"), + G_DBUS_CALL_FLAGS_NONE, -1, priv->cancel, + on_poll_position_done, self); + + return G_SOURCE_CONTINUE; +} + + +#define POLLER_INTERVAL 1 /* seconds */ +static void +start_pos_poller (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + if (!gtk_widget_get_visible (priv->box_pos_len)) { + g_debug ("box_pos_len not visible, not starting position poller"); + return; + } + if (priv->pos_poller_id != 0) { + g_debug ("Position poller already running"); + return; + } + g_debug ("Starting position poller"); + poll_position (self); + priv->pos_poller_id = g_timeout_add_seconds (POLLER_INTERVAL, + (GSourceFunc) poll_position, + self); + g_source_set_name_by_id (priv->pos_poller_id, "[PhoshMediaPlayer] pos_poller"); +} + + +static void +on_play_pause_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusMediaPlayer2Player *player = PHOSH_DBUS_MEDIA_PLAYER2_PLAYER (source_object); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (player)); + + if (!phosh_dbus_media_player2_player_call_play_pause_finish (player, res, &err)) + phosh_async_error_warn (err, "Failed to trigger play/pause"); +} + + +static void +btn_play_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (priv->player)); + + g_debug ("Play/pause"); + phosh_media_player_toggle_play_pause (self); +} + + +static void +on_next_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusMediaPlayer2Player *player = PHOSH_DBUS_MEDIA_PLAYER2_PLAYER (source_object); + PhoshMediaPlayer *self = PHOSH_MEDIA_PLAYER (user_data); + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (player)); + + if (!phosh_dbus_media_player2_player_call_next_finish (player, res, &err)) { + phosh_async_error_warn (err, "Failed to trigger next"); + return; + } + priv->track_position = 0; +} + + +static void +btn_next_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (priv->player)); + + g_debug ("next"); + phosh_dbus_media_player2_player_call_next (priv->player, priv->cancel, on_next_done, self); +} + + +static void +on_previous_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusMediaPlayer2Player *player = PHOSH_DBUS_MEDIA_PLAYER2_PLAYER (source_object); + PhoshMediaPlayer *self = PHOSH_MEDIA_PLAYER (user_data); + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (player)); + + if (!phosh_dbus_media_player2_player_call_previous_finish (player, res, &err)) { + phosh_async_error_warn (err, "Failed to trigger prev"); + return; + } + priv->track_position = 0; +} + + +static void +btn_prev_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (priv->player)); + + g_debug ("prev"); + phosh_dbus_media_player2_player_call_previous (priv->player, + priv->cancel, + on_previous_done, + self); +} + + +static void +on_seek_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusMediaPlayer2Player *player = PHOSH_DBUS_MEDIA_PLAYER2_PLAYER (source_object); + PhoshMediaPlayer *self = PHOSH_MEDIA_PLAYER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (player)); + + if (!phosh_dbus_media_player2_player_call_seek_finish (player, res, &err)) { + g_warning ("Failed to trigger seek: %s", err->message); + return; + } + poll_position (self); +} + + +static void +btn_seek_backward_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (priv->player)); + + g_debug ("seek backward for %ds", SEEK_BACK / SEEK_SECOND); + phosh_dbus_media_player2_player_call_seek (priv->player, + SEEK_BACK, + priv->cancel, + on_seek_done, + self); +} + + +static void +btn_seek_forward_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_DBUS_IS_MEDIA_PLAYER2_PLAYER (priv->player)); + + g_debug ("seek forward by %ds", SEEK_FORWARD / SEEK_SECOND); + phosh_dbus_media_player2_player_call_seek (priv->player, + SEEK_FORWARD, + priv->cancel, + on_seek_done, + self); +} + + +static void +on_raise_done (GObject *object, GAsyncResult *res, gpointer data) +{ + PhoshMprisManager *manager = PHOSH_MPRIS_MANAGER (object); + g_autoptr (GError) err = NULL; + PhoshMediaPlayer *self; + + if (!phosh_mpris_manager_raise_finish (manager, res, &err)) { + phosh_async_error_warn (err, "Failed to raise player"); + return; + } + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (data)); + self = PHOSH_MEDIA_PLAYER (data); + g_signal_emit (self, signals[PLAYER_RAISED], 0); +} + + +static void +btn_details_clicked_cb (PhoshMediaPlayer *self, GtkButton *button) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (priv->manager)); + + if (!phosh_mpris_manager_get_can_raise (priv->manager)) + return; + + phosh_mpris_manager_raise_async (priv->manager, priv->cancel, on_raise_done, self); +} + + +static GdkPixbuf * +center_pixbuf (GdkPixbuf *pixbuf) +{ + int width, height, size; + g_autoptr (GdkPixbuf) centered = NULL; + g_autoptr (cairo_t) cr = NULL; + g_autoptr (cairo_surface_t) surface = NULL; + + g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL); + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + if (width == height) + return g_object_ref (pixbuf); + + size = MAX (width, height); + /* gdk_pixbuf_copy_area would work as well but that goes via gdk_pixbuf_scale, …*/ + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + if (width > height) + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, (width - height) / 2.0); + else + gdk_cairo_set_source_pixbuf (cr, pixbuf, (height - width) / 2.0, 0); + cairo_paint (cr); + + return gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); +} + + +static GdkPixbuf * +round_corners (GdkPixbuf *pixbuf) +{ + g_autoptr (GdkPixbuf) rounded = NULL; + g_autoptr (cairo_t) cr = NULL; + g_autoptr (cairo_surface_t) surface = NULL; + int width, height, size; + double radius; + const double degrees = M_PI / 180.0; + + g_return_val_if_fail (GDK_IS_PIXBUF (pixbuf), NULL); + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + /* We only round square images */ + g_return_val_if_fail (width == height, NULL); + + size = width; + surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, size, size); + cr = cairo_create (surface); + + radius = size / 8.0; + cairo_new_path (cr); + cairo_arc (cr, size - radius, radius, radius, -90 * degrees, 0 * degrees); + cairo_arc (cr, size - radius, size - radius, radius, 0 * degrees, 90 * degrees); + cairo_arc (cr, radius, size - radius, radius, 90 * degrees, 180 * degrees); + cairo_arc (cr, radius, radius, radius, 180 * degrees, 270 * degrees); + cairo_close_path (cr); + cairo_clip (cr); + + gdk_cairo_set_source_pixbuf (cr, pixbuf, 0, 0); + cairo_paint (cr); + + return gdk_pixbuf_get_from_surface (surface, 0, 0, size, size); +} + + +static void +phosh_media_player_set_image (PhoshMediaPlayer *self, GdkPixbuf *pixbuf) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autoptr (GdkPixbuf) centered = NULL; + g_autoptr (GdkPixbuf) rounded = NULL; + + centered = center_pixbuf (pixbuf); + rounded = round_corners (centered); + g_object_set (priv->img_art, "gicon", rounded, NULL); +} + + +static void +on_fetch_icon_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMediaPlayer *self; + PhoshMediaPlayerPrivate *priv; + g_autoptr (GInputStream) stream = NULL; + g_autoptr (GError) err = NULL; + g_autofree char *type = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + + stream = g_loadable_icon_load_finish (G_LOADABLE_ICON (source_object), res, &type, &err); + if (!stream) { + g_warning ("Failed to fetch icon: %s", err->message); + return; + } + + g_debug ("Loading icon of type: %s", type); + self = PHOSH_MEDIA_PLAYER (user_data); + priv = phosh_media_player_get_instance_private (self); + pixbuf = gdk_pixbuf_new_from_stream (stream, priv->fetch_icon_cancel, &err); + + phosh_media_player_set_image (self, pixbuf); +} + + +static void +fetch_icon_async (PhoshMediaPlayer *self, const char *url) + +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autoptr (GFile) file = g_file_new_for_uri (url); + GIcon *icon; + + g_debug ("Fetching icon for %s", url); + + if (!priv->fetch_icon_cancel) + priv->fetch_icon_cancel = g_cancellable_new (); + + icon = g_file_icon_new (file); + g_loadable_icon_load_async (G_LOADABLE_ICON (icon), + ART_PIXEL_SIZE, + priv->fetch_icon_cancel, + on_fetch_icon_ready, + self); +} + + +static void +on_load_icon_from_file_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMediaPlayer *self; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GError) err = NULL; + + pixbuf = gdk_pixbuf_new_from_stream_finish (res, &err); + if (!pixbuf) { + phosh_async_error_warn (err, "Failed to load image"); + return; + } + + self = PHOSH_MEDIA_PLAYER (user_data); + phosh_media_player_set_image (self, pixbuf); +} + + +static void +phosh_media_player_load_icon_from_file_async (PhoshMediaPlayer *self, GFile *file) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + g_autoptr (GFileInputStream) stream = NULL; + g_autoptr (GError) err = NULL; + + stream = g_file_read (file, NULL, &err); + if (stream == NULL) { + g_autofree char *path = g_file_get_path (file); + + g_warning ("Failed to open '%s': %s", path, err->message); + return; + } + + if (!priv->fetch_icon_cancel) + priv->fetch_icon_cancel = g_cancellable_new (); + + gdk_pixbuf_new_from_stream_async (G_INPUT_STREAM (stream), + priv->fetch_icon_cancel, + on_load_icon_from_file_ready, + self); +} + + +static gboolean +phosh_media_player_load_icon (PhoshMediaPlayer *self, const char *url) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + gboolean has_art = FALSE; + + if (!g_set_str (&priv->url, url)) { + g_debug ("Media URL did not change, skippig load"); + return TRUE; + } + + g_debug ("Loading '%s'", url); + /* Cancel any pending icon loads */ + g_cancellable_cancel (priv->fetch_icon_cancel); + g_clear_object (&priv->fetch_icon_cancel); + + if (url && g_strcmp0 (g_uri_peek_scheme (url), "file") == 0) { + g_autoptr (GFile) file = g_file_new_for_uri (url); + + phosh_media_player_load_icon_from_file_async (self, file); + } else if (url && (g_strcmp0 (g_uri_peek_scheme (url), "http") == 0 || + g_strcmp0 (g_uri_peek_scheme (url), "https") == 0)) { + fetch_icon_async (self, url); + } else if (url && g_strcmp0 (g_uri_peek_scheme (url), "data") == 0) { + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GError) error = NULL; + + pixbuf = phosh_util_data_uri_to_pixbuf (url, &error); + if (pixbuf) { + phosh_media_player_set_image (self, pixbuf); + has_art = TRUE; + } else { + g_warning_once ("Failed to load album art from base64 string: %s", error->message); + } + } + + return has_art; +} + + +static void +on_metadata_changed (PhoshMediaPlayer *self, GParamSpec *psepc, PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + GVariant *metadata; + char *title = NULL; + char *url = NULL; + gint64 length = -1; + g_auto (GStrv) artist = NULL; + g_auto (GVariantDict) dict = G_VARIANT_DICT_INIT (NULL); + gboolean has_art = FALSE; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_debug ("Updating metadata"); + + metadata = phosh_dbus_media_player2_player_get_metadata (player); + + if (metadata) { + g_variant_dict_init (&dict, metadata); + g_variant_dict_lookup (&dict, "xesam:title", "&s", &title); + g_variant_dict_lookup (&dict, "xesam:artist", "^as", &artist); + g_variant_dict_lookup (&dict, "mpris:artUrl", "&s", &url); + g_variant_dict_lookup (&dict, "mpris:length", "x", &length); + } + + if (title) { + gtk_label_set_label (GTK_LABEL (priv->lbl_title), title); + } else { + /* Translators: Used when the title of a song is unknown */ + gtk_label_set_label (GTK_LABEL (priv->lbl_title), _("Unknown Title")); + } + + if (artist && g_strv_length (artist) > 0) { + g_autofree char *artists = g_strjoinv (", ", artist); + gtk_label_set_label (GTK_LABEL (priv->lbl_artist), artists); + } else { + /* Translators: Used when the artist of a song is unknown */ + gtk_label_set_label (GTK_LABEL (priv->lbl_artist), _("Unknown Artist")); + } + + if (length >= 0) { + g_autofree char *length_text = cui_call_format_duration ((double) length / G_USEC_PER_SEC); + gtk_label_set_label (GTK_LABEL (priv->lbl_length), length_text); + g_debug ("Metadata has length, showing box_pos_len"); + gtk_widget_set_visible (priv->box_pos_len, TRUE); + if (priv->status == PHOSH_MEDIA_PLAYER_STATUS_PLAYING) + start_pos_poller (self); + } else { + gtk_label_set_label (GTK_LABEL (priv->lbl_length), "-"); + } + priv->track_length = length; + update_position (self); + + has_art = phosh_media_player_load_icon (self, url); + + if (!has_art) + g_object_set (priv->img_art, "icon-name", "audio-x-generic-symbolic", NULL); +} + + +static void +on_playback_status_changed (PhoshMediaPlayer *self, + GParamSpec *psepc, + PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + const char *status, *icon = "media-playback-start-symbolic"; + PhoshMediaPlayerStatus current; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + + status = phosh_dbus_media_player2_player_get_playback_status (player); + + /* No mpris running, widget will not be shown */ + if (status == NULL) + return; + + g_debug ("Status: '%s'", status); + current = priv->status; + if (!g_strcmp0 ("Playing", status)) { + priv->status = PHOSH_MEDIA_PLAYER_STATUS_PLAYING; + icon = "media-playback-pause-symbolic"; + start_pos_poller (self); + } else if (!g_strcmp0 ("Paused", status)) { + priv->status = PHOSH_MEDIA_PLAYER_STATUS_PAUSED; + stop_pos_poller (self); + poll_position (self); + } else if (!g_strcmp0 ("Stopped", status)) { + priv->status = PHOSH_MEDIA_PLAYER_STATUS_STOPPED; + stop_pos_poller (self); + priv->track_position = 0; + update_position (self); + } else { + g_warning ("Unknown status %s", status); + g_warn_if_reached (); + } + + if (priv->status != current) { + g_object_set (priv->img_play, "icon-name", icon, NULL); + gtk_widget_set_valign (priv->img_play, GTK_ALIGN_START); + } +} + + +static void +on_can_go_next_changed (PhoshMediaPlayer *self, + GParamSpec *psepc, + PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + gboolean sensitive; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + sensitive = phosh_dbus_media_player2_player_get_can_go_next (player); + g_debug ("Can go next: %d", sensitive); + gtk_widget_set_sensitive (priv->btn_next, sensitive); +} + + +static void +on_can_go_previous_changed (PhoshMediaPlayer *self, + GParamSpec *psepc, + PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + gboolean sensitive; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + sensitive = phosh_dbus_media_player2_player_get_can_go_previous (player); + g_debug ("Can go prev: %d", sensitive); + gtk_widget_set_sensitive (priv->btn_prev, sensitive); +} + + +static void +on_can_play (PhoshMediaPlayer *self, GParamSpec *psepc, PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + gboolean can_play; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + can_play = phosh_dbus_media_player2_player_get_can_play (player); + g_debug ("Can play: %d", can_play); + gtk_widget_set_sensitive (priv->btn_play, can_play); + set_playable (self, can_play); +} + + +static void +on_can_seek (PhoshMediaPlayer *self, GParamSpec *psepc, PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + gboolean sensitive; + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + sensitive = phosh_dbus_media_player2_player_get_can_seek (player); + g_debug ("Can seek: %d", sensitive); + gtk_widget_set_sensitive (priv->btn_seek_backward, sensitive); + gtk_widget_set_sensitive (priv->btn_seek_forward, sensitive); +} + + +static void +phosh_media_player_dispose (GObject *object) +{ + PhoshMediaPlayer *self = PHOSH_MEDIA_PLAYER (object); + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + stop_pos_poller (self); + g_cancellable_cancel (priv->cancel); + g_clear_object (&priv->cancel); + + g_cancellable_cancel (priv->fetch_icon_cancel); + g_clear_object (&priv->fetch_icon_cancel); + + g_clear_object (&priv->manager); + g_clear_object (&priv->player); + + g_clear_pointer (&priv->url, g_free); + + G_OBJECT_CLASS (phosh_media_player_parent_class)->dispose (object); +} + + +static void +phosh_media_player_class_init (PhoshMediaPlayerClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_media_player_dispose; + object_class->get_property = phosh_media_player_get_property; + + /** + * PhoshMediaPlayer:attached + * + * Whether a player is attacked. This is %TRUE when we + * found a suitable player on the session bus. + */ + props[PROP_ATTACHED] = + g_param_spec_boolean ("attached", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshMediaPlayer:playable + * + * Whether the player has a playable track. This is mostly + * useful to ignore states where the player does not know + * about any track and so no sensible information can be + * shown. + */ + props[PROP_PLAYABLE] = + g_param_spec_boolean ("playable", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshMediaPlayer::player-raised: + * + * The player was raised to the user + */ + signals[PLAYER_RAISED] = g_signal_new ("player-raised", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_css_name (widget_class, "phosh-media-player"); + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/media-player.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_next); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_play); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_prev); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_details); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_seek_backward); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, btn_seek_forward); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, img_art); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, img_play); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, lbl_artist); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, lbl_title); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, box_pos_len); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, lbl_position); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, lbl_length); + gtk_widget_class_bind_template_child_private (widget_class, PhoshMediaPlayer, prb_position); + gtk_widget_class_bind_template_callback (widget_class, btn_play_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, btn_next_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, btn_prev_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, btn_details_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, btn_seek_backward_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, btn_seek_forward_clicked_cb); +} + + +static void +phosh_media_player_init (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + PhoshMprisManager *manager = phosh_shell_get_mpris_manager (phosh_shell_get_default ()); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->cancel = g_cancellable_new (); + priv->track_length = -1; + priv->track_position = -1; + priv->pos_poller_id = 0; + + if (manager) { + priv->manager = g_object_ref (manager); + + g_object_bind_property (priv->manager, + "can-raise", + priv->btn_details, + "sensitive", + G_BINDING_DEFAULT); + } +} + + +GtkWidget * +phosh_media_player_new (void) +{ + return g_object_new (PHOSH_TYPE_MEDIA_PLAYER, NULL); +} + + +gboolean +phosh_media_player_get_is_playable (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MEDIA_PLAYER (self), FALSE); + + return priv->playable; +} + + +PhoshMediaPlayerStatus +phosh_media_player_get_status (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MEDIA_PLAYER (self), PHOSH_MEDIA_PLAYER_STATUS_STOPPED); + + return priv->status; +} + + +void +phosh_media_player_toggle_play_pause (PhoshMediaPlayer *self) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + + phosh_dbus_media_player2_player_call_play_pause (priv->player, + priv->cancel, + on_play_pause_done, + self); +} + + +void +phosh_media_player_set_player (PhoshMediaPlayer *self, PhoshDBusMediaPlayer2Player *player) +{ + PhoshMediaPlayerPrivate *priv = phosh_media_player_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_MEDIA_PLAYER (self)); + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (priv->manager)); + + if (priv->player) + g_signal_handlers_disconnect_by_data (priv->player, self); + + g_set_object (&priv->player, player); + + if (!priv->player) { + set_attached (self, FALSE); + return; + } + + g_debug ("Connected player %p", priv->player); + g_object_connect (priv->player, + "swapped-object-signal::notify::metadata", + G_CALLBACK (on_metadata_changed), + self, + "swapped-object-signal::notify::playback-status", + G_CALLBACK (on_playback_status_changed), + self, + "swapped-object-signal::notify::can-go-next", + G_CALLBACK (on_can_go_next_changed), + self, + "swapped-object-signal::notify::can-go-previous", + G_CALLBACK (on_can_go_previous_changed), + self, + "swapped-object-signal::notify::can-play", + G_CALLBACK (on_can_play), + self, + "swapped-object-signal::notify::can-seek", + G_CALLBACK (on_can_seek), + self, + NULL); + + /* Set 'attached' before running notifiers, since we check it on e.g. start_pos_poller() */ + set_attached (self, TRUE); + /* Hide progress bar box by default, it's shown if track length is given in metadata */ + gtk_widget_set_visible (priv->box_pos_len, FALSE); + + g_object_notify (G_OBJECT (priv->player), "metadata"); + g_object_notify (G_OBJECT (priv->player), "playback-status"); + g_object_notify (G_OBJECT (priv->player), "can-go-next"); + g_object_notify (G_OBJECT (priv->player), "can-go-previous"); + g_object_notify (G_OBJECT (priv->player), "can-play"); + g_object_notify (G_OBJECT (priv->player), "can-seek"); +} diff --git a/src/media-player.h b/src/media-player.h new file mode 100644 index 000000000..8f1b04214 --- /dev/null +++ b/src/media-player.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "mpris-dbus.h" + +#include + +G_BEGIN_DECLS + +/** + * PhoshMediaPlayerStatus: + * @PHOSH_MEDIA_PLAYER_STATUS_STOPPED: The player is stopped. + * @PHOSH_MEDIA_PLAYER_STATUS_PAUSED: The player is paused. + * @PHOSH_MEDIA_PLAYER_STATUS_PLAYING: The player is playing. + * + * The status of the media player attached to the wigget + */ +typedef enum { + PHOSH_MEDIA_PLAYER_STATUS_STOPPED, + PHOSH_MEDIA_PLAYER_STATUS_PAUSED, + PHOSH_MEDIA_PLAYER_STATUS_PLAYING, +} PhoshMediaPlayerStatus; + +#define PHOSH_TYPE_MEDIA_PLAYER (phosh_media_player_get_type ()) + +struct _PhoshMediaPlayerClass { + GtkGridClass parent_class; +}; + +G_DECLARE_DERIVABLE_TYPE (PhoshMediaPlayer, phosh_media_player, PHOSH, MEDIA_PLAYER, GtkGrid) + +GtkWidget *phosh_media_player_new (void); +void phosh_media_player_set_player (PhoshMediaPlayer *self, + PhoshDBusMediaPlayer2Player *player); +gboolean phosh_media_player_get_is_playable (PhoshMediaPlayer *self); +PhoshMediaPlayerStatus phosh_media_player_get_status (PhoshMediaPlayer *self); +void phosh_media_player_toggle_play_pause (PhoshMediaPlayer *self); + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 000000000..0434518f9 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,681 @@ +subdir('dbus') +subdir('monitor') +subdir('notifications') +subdir('wwan') +subdir('search') +subdir('settings') +subdir('gtk-list-models') + +phosh_enums_header_subdir = 'phosh' + +install_headers(schema_enum_headers, subdir: phosh_enums_header_subdir) + +plugin_exported_symbols = 'phosh-exported-symbols.txt' +libphosh_exported_symbols = 'libphosh.syms' + +dynamic_list = custom_target( + 'build-dynamic-list', + command: ['sh', '-c', 'echo "{"; cat @INPUT@; echo "};"'], + capture: true, + input: '@0@.in'.format(plugin_exported_symbols), + output: plugin_exported_symbols, +) + +symbols_file = custom_target( + 'build-symbols-file', + command: [ + '@0@/tools/build-symbols-file'.format(meson.project_source_root()), + '@0@'.format(libphosh_api_version.replace('.', '_')), + '@INPUT0@', + '@INPUT1@', + ], + capture: true, + input: [ + '@0@.in'.format(plugin_exported_symbols), + '@0@.in'.format(libphosh_exported_symbols), + ], + output: libphosh_exported_symbols, +) + +pkgconfig.generate( + requires: gsettings_desktop_schemas_dep, + subdirs: phosh_enums_header_subdir, + version: meson.project_version(), + install_dir: libdir / 'pkgconfig', + filebase: 'phosh-settings', + name: 'phosh-settings-schemas', + description: 'Shared gsettings schema and enum headers for Phosh', +) + +phosh_resources = gnome.compile_resources( + 'phosh-resources', + 'phosh.gresources.xml', + c_name: 'phosh', +) + +libphosh_enum_headers = files('lockscreen.h') + +phosh_enum_headers = files( + 'animation.h', + 'app-grid-button.h', + 'drag-surface.h', + 'gnome-shell-manager.h', + 'home.h', + 'mode-manager.h', + 'monitor/monitor.h', + 'notifications/notification.h', + 'notifications/notify-manager.h', + 'phosh-wayland.h', + 'rotation-manager.h', + 'shell-priv.h', + 'top-panel.h', +) + [ + libphosh_enum_headers, + schema_enum_headers, +] + +phosh_enums = gnome.mkenums_simple('phosh-enums', sources: phosh_enum_headers) + +libphosh_enums = gnome.mkenums_simple( + 'libphosh-enums', + sources: libphosh_enum_headers, + install_header: bindings_lib, + install_dir: lib_inc_dir, +) + +phosh_settings_sources = files('settings.c') + [phosh_settings_widgets_sources] + +phosh_marshalers = gnome.genmarshal( + 'phosh-marshalers', + sources: 'phosh-marshalers.list', + prefix: '_phosh_marshal', + valist_marshallers: true, +) + + +# sources generated with glib tooling +# hence parsable by gi-docgen +phosh_glib_generated_sources = [ + phosh_enums[0], + phosh_marshalers[0], + phosh_resources[0], + generated_dbus_sources, +] + +phosh_glib_generated_headers = [ + phosh_enums[1], + phosh_marshalers[1], + phosh_resources[1], + generated_dbus_headers, +] + +phosh_generated_sources = [phosh_glib_generated_sources, wl_proto_sources] + +phosh_generated_headers = [phosh_glib_generated_headers, wl_proto_headers] + +phosh_tool_headers = files( + 'activity.h', + 'ambient.h', + 'animation.h', + 'app-auth-prompt.h', + 'app-grid-base-button.h', + 'app-grid-button.h', + 'app-grid-folder-button.h', + 'app-grid.h', + 'app-list-model.h', + 'audio-manager.h', + 'auth-prompt-option.h', + 'auto-brightness-bucket.h', + 'auto-brightness.h', + 'background-cache.h', + 'background-image.h', + 'background.h', + 'battery-manager.h', + 'bidi.h', + 'brightness-settings.h', + 'call-notification.h', + 'call.h', + 'calls-manager.h', + 'cell-broadcast-manager.h', + 'clamp.h', + 'connectivity-info.h', + 'connectivity-manager.h', + 'debug-control.h', + 'default-media-player.h', + 'docked-info.h', + 'docked-manager.h', + 'drag-surface.h', + 'emergency-contact-row.h', + 'emergency-contact.h', + 'emergency-menu.h', + 'end-session-dialog.h', + 'fading-label.h', + 'favorite-list-model.h', + 'feedback-manager.h', + 'folder-info.h', + 'gnome-shell-manager.h', + 'gtk-mount-manager.h', + 'gtk-mount-prompt.h', + 'hks-info.h', + 'hks-manager.h', + 'keypad.h', + 'launcher-entry-manager.h', + 'lockshield.h', + 'manager.h', + 'media-player.h', + 'mode-manager.h', + 'mount-manager.h', + 'mount-operation.h', + 'osd-window.h', + 'overview.h', + 'password-entry.h', + 'phosh-wayland.h', + 'plugin-loader.h', + 'power-menu-manager.h', + 'power-menu.h', + 'quick-settings-box.h', + 'quick-settings.h', + 'revealer.h', + 'splash-manager.h', + 'splash.h', + 'status-page-placeholder.h', + 'suspend-manager.h', + 'swipe-away-bin.h', + 'system-modal-dialog.h', + 'system-modal.h', + 'udev-manager.h', + 'util.h', + 'vpn-info.h', + 'vpn-manager.h', + 'widget-box.h', + 'wl-buffer.h', +) + +# Available in tools and unit tests: +phosh_tool_sources = files( + 'activity.c', + 'ambient.c', + 'animation.c', + 'app-auth-prompt.c', + 'app-grid-base-button.c', + 'app-grid-button.c', + 'app-grid-folder-button.c', + 'app-grid.c', + 'app-list-model.c', + 'audio-manager.c', + 'audio/audio-device.c', + 'audio/audio-devices.c', + 'auth-prompt-option.c', + 'auto-brightness-bucket.c', + 'auto-brightness.c', + 'background-cache.c', + 'background-image.c', + 'background.c', + 'backlight-sysfs.c', + 'backlight.c', + 'battery-manager.c', + 'bidi.c', + 'brightness-manager.c', + 'brightness-settings.c', + 'call-notification.c', + 'call.c', + 'calls-manager.c', + 'cell-broadcast-manager.c', + 'cell-broadcast-prompt.c', + 'clamp.c', + 'connectivity-info.c', + 'connectivity-manager.c', + 'debug-control.c', + 'default-media-player.c', + 'docked-info.c', + 'docked-manager.c', + 'drag-surface.c', + 'emergency-contact-row.c', + 'emergency-contact.c', + 'emergency-menu.c', + 'end-session-dialog.c', + 'fading-label.c', + 'favorite-list-model.c', + 'feedback-manager.c', + 'feedback-status-page.c', + 'folder-info.c', + 'gnome-shell-manager.c', + 'gtk-mount-manager.c', + 'gtk-mount-prompt.c', + 'hks-info.c', + 'hks-manager.c', + 'keypad.c', + 'launcher-entry-manager.c', + 'layersurface.c', + 'lockshield.c', + 'manager.c', + 'media-player.c', + 'metainfo-cache.c', + 'mode-manager.c', + 'mount-manager.c', + 'mount-operation.c', + 'mpris-manager.c', + 'osd-window.c', + 'overview.c', + 'password-entry.c', + 'phosh-wayland.c', + 'plugin-loader.c', + 'power-menu-manager.c', + 'power-menu.c', + 'quick-setting.c', + 'quick-settings-box.c', + 'quick-settings.c', + 'revealer.c', + 'splash-manager.c', + 'splash.c', + 'status-icon.c', + 'status-page-placeholder.c', + 'status-page.c', + 'style-manager.c', + 'suspend-manager.c', + 'swipe-away-bin.c', + 'system-modal-dialog.c', + 'system-modal.c', + 'udev-manager.c', + 'util.c', + 'vpn-info.c', + 'vpn-manager.c', + 'wall-clock.c', + 'widget-box.c', + 'wifi-info.c', + 'wifi-manager.c', + 'wifi-network-row.c', + 'wifi-network.c', + 'wifi-status-page.c', + 'wl-buffer.c', +) + [ + phosh_monitor_sources, + phosh_notifications_sources, +] + +# Headers added here will be introspected and included in the GIR. +# Only API that is considered public to libphosh consumers should be added here. +# This list *must* be kept in sync with libphosh.h +libphosh_headers = files( + 'layersurface.h', + 'lockscreen-manager.h', + 'lockscreen.h', + 'quick-setting.h', + 'screenshot-manager.h', + 'shell.h', + 'status-icon.h', + 'status-page.h', + 'wall-clock.h', +) + +phosh_headers = files( + 'app-tracker.h', + 'arrow.h', + 'audio/audio-device.h', + 'audio/audio-devices.h', + 'auth.h', + 'background-manager.h', + 'backlight-sysfs.h', + 'backlight.h', + 'battery-info.h', + 'brightness-manager.h', + 'bt-device-row.h', + 'bt-info.h', + 'bt-manager.h', + 'bt-status-page.h', + 'cell-broadcast-prompt.h', + 'contrib/shell-network-agent.h', + 'emergency-calls-manager.h', + 'fader.h', + 'fake-clock.h', + 'feedback-status-page.h', + 'feedbackinfo.h', + 'home.h', + 'idle-manager.h', + 'keyboard-events.h', + 'layout-manager.h', + 'location-info.h', + 'location-manager.h', + 'lockscreen-bg.h', + 'metainfo-cache.h', + 'monitor-manager.h', + 'network-auth-manager.h', + 'network-auth-prompt.h', + 'osk-manager.h', + 'polkit-auth-agent.h', + 'polkit-auth-prompt.h', + 'portal-access-manager.h', + 'portal-request.h', + 'proximity.h', + 'rotateinfo.h', + 'rotation-manager.h', + 'run-command-dialog.h', + 'run-command-manager.h', + 'screen-saver-manager.h', + 'sensor-proxy-manager.h', + 'session-manager.h', + 'session-presence.h', + 'shell.h', + 'style-manager.h', + 'system-prompt.h', + 'system-prompter.h', + 'thumbnail-priv.h', + 'thumbnail.h', + 'top-panel-bg.h', + 'top-panel.h', + 'toplevel-manager.h', + 'toplevel-thumbnail.h', + 'toplevel.h', + 'torch-info.h', + 'torch-manager.h', + 'wifi-info.h', + 'wifi-manager.h', + 'wifi-network-row.h', + 'wifi-network.h', + 'wifi-status-page.h', + 'wwan-info.h', +) + [ + libphosh_headers, + phosh_monitor_headers, + phosh_notifications_headers, + phosh_settings_widgets_headers, + phosh_wwan_headers, +] + +# Symbols from these are not available in tools and unit tests +# Prefer adding to phosh_tool_sources +phosh_sources = files( + 'app-tracker.c', + 'arrow.c', + 'auth.c', + 'background-manager.c', + 'battery-info.c', + 'bt-device-row.c', + 'bt-info.c', + 'bt-manager.c', + 'bt-status-page.c', + 'contrib/shell-network-agent.c', + 'emergency-calls-manager.c', + 'fader.c', + 'fake-clock.c', + 'feedbackinfo.c', + 'home.c', + 'idle-manager.c', + 'keyboard-events.c', + 'layout-manager.c', + 'location-info.c', + 'location-manager.c', + 'lockscreen-bg.c', + 'lockscreen-manager.c', + 'lockscreen.c', + 'monitor-manager.c', + 'network-auth-manager.c', + 'network-auth-prompt.c', + 'osk-manager.c', + 'polkit-auth-agent.c', + 'polkit-auth-prompt.c', + 'portal-access-manager.c', + 'portal-request.c', + 'proximity.c', + 'rotateinfo.c', + 'rotation-manager.c', + 'run-command-dialog.c', + 'run-command-manager.c', + 'screen-saver-manager.c', + 'screenshot-manager.c', + 'sensor-proxy-manager.c', + 'session-manager.c', + 'session-presence.c', + 'shell.c', + 'system-prompt.c', + 'system-prompter.c', + 'thumbnail.c', + 'top-panel-bg.c', + 'top-panel.c', + 'toplevel-manager.c', + 'toplevel-thumbnail.c', + 'toplevel.c', + 'torch-info.c', + 'torch-manager.c', + 'wwan-info.c', +) + [ + phosh_settings_sources, + phosh_wwan_sources, +] + +# Headers are bundled as they're not shipped by gnome-bluetooth +# https://gitlab.gnome.org/GNOME/gnome-bluetooth/-/merge_requests/200 +gnome_bluetooth_headers_dep = declare_dependency( + include_directories: 'contrib/gnome-bluetooth', +) +# We build our own dep to avoid `-Wmissing-include-dirs` as the +# directory in the Cflags of gnome-bluetooth-3.0.pc do not +# necessarily exist +gnome_bluetooth_custom_dep = gnome_bluetooth_dep.partial_dependency( + link_args: true, + links: true, +) + +phosh_deps = [ + appstream_dep, + libsoup_dep, + fribidi_dep, + gcr_dep, + gio_dep, + gio_unix_dep, + glib_dep, + gmodule_dep, + gmobile_dep, + gnome_bluetooth_custom_dep, + gnome_bluetooth_headers_dep, + gnome_desktop_dep, + gobject_dep, + gsettings_desktop_schemas_dep, + gtk_dep, + gtk_wayland_dep, + libcall_ui_dep, + gudev_dep, + libfeedback_dep, + libgvc_dep, + libhandy_dep, + libnm_dep, + libpolkit_agent_dep, + libsystemd_dep, + mm_glib_dep, + network_agent_dep, + upower_glib_dep, + wayland_client_dep, + cc.find_library('pam', required: true), + cc.find_library('m', required: false), + cc.find_library('rt', required: false), +] + +phosh_inc = include_directories('.') +phosh_lib_inc = [ + root_inc, + phosh_inc, + phosh_monitor_inc, + phosh_notifications_inc, + phosh_wwan_inc, + proto_inc, + dbus_inc, + phosh_gtk_list_models_inc, +] + +# A static library used by tests and tools +phosh_tool_lib = static_library( + 'phosh-tool', + [ + phosh_tool_sources, + phosh_tool_headers, + phosh_generated_headers, + phosh_generated_sources, + phosh_gtk_list_models_sources, + ], + include_directories: phosh_lib_inc, + dependencies: phosh_deps, +) +phosh_tool_dep = declare_dependency( + sources: [ + phosh_generated_headers, + # Ensure the resources are available to indirect users too + phosh_resources[0], + ], + include_directories: phosh_lib_inc, + link_with: phosh_tool_lib, + dependencies: phosh_deps, +) + +# A library containing everything +phosh_lib = both_libraries( + 'phosh-@0@'.format(libphosh_api_version), + phosh_headers, + phosh_sources, + sources: [phosh_generated_headers, generated_dbus_sources], + include_directories: phosh_lib_inc, + dependencies: [phosh_tool_dep, phosh_deps], + soversion: 0, + install: bindings_lib, + link_args: '-Wl,--version-script,@0@'.format(symbols_file.full_path()), + link_depends: symbols_file, + override_options: abi_check ? {'optimization': 'g'} : {}, +) + +if abi_check + abi_compliance_checker = find_program('abi-compliance-checker') + abi_dumper = find_program('abi-dumper') + + header_list = custom_target( + 'public-header-list', + input: libphosh_headers, + output: 'libphosh-public-headers.txt', + capture: true, + command: ['ls', '-1', '@INPUT@'], + ) + + abi_dump = custom_target( + 'abi-dump', + input: [phosh_lib, header_list], + output: 'libphosh-abi.dump', + command: [ + abi_dumper, + '@INPUT0@', + '-public-headers', + '@INPUT1@', + '-o', + '@OUTPUT@', + '-lver', + meson.project_version(), + ], + depends: header_list, + ) + + custom_target( + 'abi-compliance', + input: ['libphosh-abi.dump', abi_dump], + output: 'abi-compliance-check-report.html', + command: [ + abi_compliance_checker, + '-report-path', + '@OUTPUT@', + '-l', + meson.project_name(), + '-old', + '@INPUT0@', + '-new', + '@INPUT1@', + ], + build_by_default: true, + ) +endif + +phosh_static_lib_dep = declare_dependency( + include_directories: phosh_lib_inc, + link_with: phosh_lib.get_static_lib(), + dependencies: [phosh_deps, phosh_tool_dep], +) + +# Dependencies that can be used by plugins +phosh_plugins_dep = declare_dependency( + include_directories: phosh_lib_inc, + sources: [phosh_enums[1], generated_dbus_headers, wl_proto_headers], +) + +if enable_introspection or bindings_lib + phosh_gir_sources = [ + libphosh_headers, + phosh_sources, + phosh_tool_sources, + libphosh_generated_dbus_headers, + libphosh_generated_dbus_sources, + libphosh_enums[1], + ] + + phosh_gir_extra_args = ['--quiet'] + + phosh_gir = gnome.generate_gir( + phosh_lib, + header: 'libphosh.h', + sources: phosh_gir_sources, + nsversion: '0', + namespace: 'Phosh', + export_packages: 'libphosh-@0@'.format(libphosh_api_version), + symbol_prefix: 'phosh', + identifier_prefix: 'Phosh', + link_with: phosh_lib, + includes: [ + 'Gcr-3', + 'Gio-2.0', + 'Gtk-3.0', + 'GnomeDesktop-3.0', + 'Handy-1', + 'NM-1.0', + 'GnomeBluetooth-3.0', + ], + extra_args: phosh_gir_extra_args, + dependencies: phosh_static_lib_dep, + fatal_warnings: true, + install: bindings_lib, + install_dir_gir: datadir / 'gir-1.0', + ) +endif + +if bindings_lib + pkgconfig.generate( + phosh_lib, + name: 'libphosh', + subdirs: 'libphosh-@0@'.format(libphosh_api_version), + filebase: 'libphosh-@0@'.format(libphosh_api_version), + version: meson.project_version(), + description: 'Library for building components embedding Phosh', + requires: 'phosh-settings', + url: 'https://world.pages.gitlab.gnome.org/Phosh/phosh/', + ) + + install_headers(['libphosh.h', libphosh_headers], subdir: lib_inc_subdir) + + apiversion = '0.0' + if get_option('vapi') + libphosh_vapi = gnome.generate_vapi( + 'libphosh-' + libphosh_api_version, + sources: phosh_gir[0], + packages: ['gio-2.0', 'gtk+-3.0', 'gnome-desktop-3.0', 'libnm'], + install: true, + install_dir: vapidir, + metadata_dirs: [meson.current_source_dir()], + ) + endif +endif + +phosh_export_sym_link_arg = '-Wl,--dynamic-list=@0@'.format( + dynamic_list.full_path(), +) +phosh = executable( + 'phosh', + 'main.c', + dependencies: phosh_static_lib_dep, + link_depends: dynamic_list, + link_args: phosh_export_sym_link_arg, + install: true, + install_dir: libexecdir, +) diff --git a/src/metainfo-cache.c b/src/metainfo-cache.c new file mode 100644 index 000000000..7c494da59 --- /dev/null +++ b/src/metainfo-cache.c @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2025 The Phosh Authors + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-metainfo-cache" + +#include "config.h" +#include "metainfo-cache.h" +#include "util.h" + +#include + +#include + +enum { + PROP_0, + PROP_READY, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + +enum { + CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +/** + * PhoshMetainfoCache: + * + * Cache for application meta information + */ +typedef struct _PhoshMetainfoCache { + GObject parent; + + AsPool *as_pool; + GCancellable *cancel; + gboolean ready; +} PhoshMetainfoCache; + + +G_DEFINE_TYPE (PhoshMetainfoCache, phosh_metainfo_cache, G_TYPE_OBJECT) + + +static void +on_as_pool_changed (PhoshMetainfoCache *self) +{ + g_debug ("Metainfo pool changed"); + g_signal_emit (self, signals[CHANGED], 0); +} + + +static void +on_as_pool_load_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean success; + PhoshMetainfoCache *self; + g_autoptr (GError) err = NULL; + + success = as_pool_load_finish (AS_POOL (source_object), res, &err); + if (!success) { + phosh_async_error_warn (err, "Failed to load metainfo pool"); + return; + } + + g_return_if_fail (PHOSH_IS_METAINFO_CACHE (user_data)); + self = PHOSH_METAINFO_CACHE (user_data); + + g_signal_connect_object (self->as_pool, "changed", G_CALLBACK (on_as_pool_changed), self, + G_CONNECT_SWAPPED); + + self->ready = TRUE; + g_debug ("Metainfo cache loaded"); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_READY]); +} + + +static void +phosh_metainfo_cache_constructed (GObject *object) +{ + PhoshMetainfoCache *self = PHOSH_METAINFO_CACHE (object); + + G_OBJECT_CLASS (phosh_metainfo_cache_parent_class)->constructed (object); + + self->as_pool = as_pool_new (); + /* We only bother about metainfo in a non-system cache */ + as_pool_set_flags (self->as_pool, + AS_POOL_FLAG_LOAD_OS_CATALOG | AS_POOL_FLAG_LOAD_OS_DESKTOP_FILES | + AS_POOL_FLAG_LOAD_OS_METAINFO | AS_POOL_FLAG_LOAD_FLATPAK | + AS_POOL_FLAG_MONITOR); + + as_pool_load_async (self->as_pool, self->cancel, + on_as_pool_load_ready, + self); +} + + +static void +phosh_metainfo_cache_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshMetainfoCache *self = PHOSH_METAINFO_CACHE (object); + + switch (property_id) { + case PROP_READY: + g_value_set_boolean (value, self->ready); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_metainfo_cache_dispose (GObject *object) +{ + PhoshMetainfoCache *self = PHOSH_METAINFO_CACHE (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->as_pool); + + G_OBJECT_CLASS (phosh_metainfo_cache_parent_class)->dispose (object); +} + + +static void +phosh_metainfo_cache_class_init (PhoshMetainfoCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_metainfo_cache_constructed; + object_class->get_property = phosh_metainfo_cache_get_property; + object_class->dispose = phosh_metainfo_cache_dispose; + + /** + * PhoshMetainfoCache:ready: + * + * Whether the cache is ready to use + */ + props[PROP_READY] = + g_param_spec_boolean ("ready", "", "", FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[CHANGED] = g_signal_new ("changed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_metainfo_cache_init (PhoshMetainfoCache *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshMetainfoCache * +phosh_metainfo_cache_get_default (void) +{ + static PhoshMetainfoCache *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_METAINFO_CACHE, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} + +/** + * phosh_metainfo_get_data_id: + * @app_id: desktop app id + * + * Returns (transfer full): the 5-part AppStream identifier or NULL + */ +char * +phosh_metainfo_get_data_id (PhoshMetainfoCache *self, const char *app_id) +{ + g_autoptr (AsComponentBox) result = NULL; + + result = as_pool_get_components_by_launchable (self->as_pool, AS_LAUNCHABLE_KIND_DESKTOP_ID, + app_id); + + if (!result || as_component_box_is_empty (result)) + return NULL; + + return g_strdup (as_component_get_data_id (as_component_box_index_safe (result, 0))); +} diff --git a/src/metainfo-cache.h b/src/metainfo-cache.h new file mode 100644 index 000000000..027f30727 --- /dev/null +++ b/src/metainfo-cache.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2025 The Phosh Authors + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_METAINFO_CACHE phosh_metainfo_cache_get_type () + +G_DECLARE_FINAL_TYPE (PhoshMetainfoCache, phosh_metainfo_cache, + PHOSH, METAINFO_CACHE, GObject) + +PhoshMetainfoCache *phosh_metainfo_cache_get_default (void); +char *phosh_metainfo_get_data_id (PhoshMetainfoCache *self, const char *cid); + +G_END_DECLS diff --git a/src/mode-manager.c b/src/mode-manager.c new file mode 100644 index 000000000..4248c35c8 --- /dev/null +++ b/src/mode-manager.c @@ -0,0 +1,471 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-mode-manager" + +#include "phosh-config.h" + +#include "mode-manager.h" +#include "shell-priv.h" +#include "util.h" +#include "dbus/hostname1-dbus.h" + +#define BUS_NAME "org.freedesktop.hostname1" +#define OBJECT_PATH "/org/freedesktop/hostname1" + +/** + * PhoshModeManager: + * + * Determines the device mode + * + * #PhoshModeManager tracks the device mode and attached hardware. + */ + +enum { + PROP_0, + PROP_DEVICE_TYPE, + PROP_HW_FLAGS, + PROP_MIMICRY, + + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshModeManager { + PhoshManager parent; + + PhoshModeDeviceType device_type; + PhoshModeDeviceType mimicry; + PhoshModeHwFlags hw_flags; + + PhoshMonitorManager *monitor_manager; + + PhoshDBusHostname1 *proxy; + GCancellable *cancel; + char *chassis; + PhoshWaylandSeatCapabilities wl_caps; + + /* Tablet mode */ + gboolean is_tablet_mode; + struct zphoc_tablet_mode_switch_v1 *tablet_mode_switch; +}; +G_DEFINE_TYPE (PhoshModeManager, phosh_mode_manager, PHOSH_TYPE_MANAGER); + + +static void +phosh_mode_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (object); + + switch (property_id) { + case PROP_HW_FLAGS: + g_value_set_flags (value, self->hw_flags); + break; + case PROP_DEVICE_TYPE: + g_value_set_enum (value, self->device_type); + break; + case PROP_MIMICRY: + g_value_set_enum (value, self->mimicry); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static gboolean +has_external_display (PhoshModeManager *self) +{ + int n_monitors; + PhoshMonitor *primary; + PhoshShell *shell = phosh_shell_get_default (); + + n_monitors = phosh_monitor_manager_get_num_monitors (self->monitor_manager); + + /* We assume only one display can be built in */ + if (n_monitors > 1) + return TRUE; + + primary = phosh_shell_get_primary_monitor (shell); + if (primary == NULL) + return FALSE; + + /* Single monitor is builtin */ + if (phosh_shell_get_builtin_monitor (shell) == primary) + return FALSE; + + /* Single monitor is virtual (e.g. in automatic tests) */ + if (primary->conn_type == PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL) + return FALSE; + + /* primary display is not built-in */ + return TRUE; +} + + +static void +update_props (PhoshModeManager *self) +{ + PhoshModeDeviceType device_type, mimicry; + PhoshModeHwFlags hw; + + /* Self->Chassis type */ + hw = PHOSH_MODE_HW_NONE; + if (g_strcmp0 (self->chassis, "handset") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_PHONE; + } else if (g_strcmp0 (self->chassis, "laptop") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_LAPTOP; + hw |= PHOSH_MODE_HW_KEYBOARD; + } else if (g_strcmp0 (self->chassis, "desktop") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_DESKTOP; + hw |= PHOSH_MODE_HW_KEYBOARD; + } else if (g_strcmp0 (self->chassis, "convertible") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_CONVERTIBLE; + } else if (g_strcmp0 (self->chassis, "tablet") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_TABLET; + } else if (g_strcmp0 (self->chassis, "embedded") == 0) { + device_type = PHOSH_MODE_DEVICE_TYPE_EMBEDDED; + } else { + device_type = PHOSH_MODE_DEVICE_TYPE_UNKNOWN; + } + mimicry = device_type; + + /* Additional hardware */ + if (has_external_display (self)) + hw |= PHOSH_MODE_HW_EXT_DISPLAY; + + if (self->wl_caps & PHOSH_WAYLAND_SEAT_CAPABILITY_POINTER) + hw |= PHOSH_MODE_HW_POINTER; + + /* Mimicries */ + if (device_type == PHOSH_MODE_DEVICE_TYPE_PHONE && + (hw & PHOSH_MODE_DOCKED_PHONE_MASK) == PHOSH_MODE_DOCKED_PHONE_MASK) { + mimicry = PHOSH_MODE_DEVICE_TYPE_DESKTOP; + } else if (device_type == PHOSH_MODE_DEVICE_TYPE_TABLET && + (hw & PHOSH_MODE_DOCKED_TABLET_MASK) == PHOSH_MODE_DOCKED_TABLET_MASK) { + mimicry = PHOSH_MODE_DEVICE_TYPE_DESKTOP; + } else if (device_type == PHOSH_MODE_DEVICE_TYPE_EMBEDDED && + (hw & PHOSH_MODE_DOCKED_EMBEDDED_MASK) == PHOSH_MODE_DOCKED_EMBEDDED_MASK) { + mimicry = PHOSH_MODE_DEVICE_TYPE_DESKTOP; + } else if (device_type == PHOSH_MODE_DEVICE_TYPE_CONVERTIBLE) { + if (self->is_tablet_mode < 0 || self->is_tablet_mode == FALSE) + mimicry = PHOSH_MODE_DEVICE_TYPE_LAPTOP; + else + mimicry = PHOSH_MODE_DEVICE_TYPE_TABLET; + } + + g_object_freeze_notify (G_OBJECT (self)); + + if (device_type != self->device_type) { + g_autofree char *name = g_enum_to_string (PHOSH_TYPE_MODE_DEVICE_TYPE, device_type); + + self->device_type = device_type; + g_debug ("Device type is %s", name); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEVICE_TYPE]); + } + + if (mimicry != self->mimicry) { + g_autofree char *name = g_enum_to_string (PHOSH_TYPE_MODE_DEVICE_TYPE, mimicry); + + self->mimicry = mimicry; + g_debug ("Mimicry is %s", name); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MIMICRY]); + } + + if (hw != self->hw_flags) { + g_autofree char *names = g_flags_to_string (PHOSH_TYPE_MODE_HW_FLAGS, hw); + self->hw_flags = hw; + g_debug ("HW flags %s", names); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HW_FLAGS]); + } + + g_object_thaw_notify (G_OBJECT (self)); +} + + +static void +on_n_monitors_changed (PhoshModeManager *self, GParamSpec *pspec, PhoshMonitorManager *manager) +{ + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (manager)); + + update_props (self); +} + + +static void +on_chassis_changed (PhoshModeManager *self, + GParamSpec *pspec, + PhoshDBusHostname1 *proxy) +{ + const char *chassis; + + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_HOSTNAME1 (proxy)); + + chassis = phosh_dbus_hostname1_get_chassis (self->proxy); + + if (!chassis) + return; + + g_debug ("Chassis: %s", chassis); + g_free (self->chassis); + self->chassis = g_strdup (chassis); + update_props (self); +} + + +static void +tablet_mode_switch_disabled (void *data, struct zphoc_tablet_mode_switch_v1 *zphoc_tablet_mode_switch_v1) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (data); + + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + + g_debug ("Tablet mode disabled"); + + self->is_tablet_mode = FALSE; + update_props (self); +} + +static void +tablet_mode_switch_enabled (void *data, struct zphoc_tablet_mode_switch_v1 *zphoc_tablet_mode_switch_v1) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (data); + + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + + g_debug ("Tablet mode enabled"); + + self->is_tablet_mode = TRUE; + update_props (self); +} + + +const struct zphoc_tablet_mode_switch_v1_listener tablet_mode_switch_listener = { + .disabled = tablet_mode_switch_disabled, + .enabled = tablet_mode_switch_enabled, +}; + + +static void +register_tablet_mode_switch (PhoshModeManager *self) +{ + if (self->tablet_mode_switch) + return; + + self->tablet_mode_switch = zphoc_device_state_v1_get_tablet_mode_switch ( + phosh_wayland_get_zphoc_device_state_v1 (phosh_wayland_get_default ())); + zphoc_tablet_mode_switch_v1_add_listener (self->tablet_mode_switch, &tablet_mode_switch_listener, self); +} + + +static void +on_seat_capabilities_changed (PhoshModeManager *self, + GParamSpec *pspec, + PhoshWayland *wl) +{ + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + g_return_if_fail (PHOSH_IS_WAYLAND (wl)); + + self->wl_caps = phosh_wayland_get_seat_capabilities (wl); + + if (self->wl_caps & PHOSH_WAYLAND_SEAT_CAPABILITY_TABLET_MODE_SWITCH) + register_tablet_mode_switch (self); + + update_props (self); +} + + +static void +on_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + PhoshModeManager *self) +{ + g_autoptr (GError) err = NULL; + PhoshWayland *wl; + PhoshDBusHostname1 *proxy; + + proxy = phosh_dbus_hostname1_proxy_new_for_bus_finish (res, &err); + if (proxy == NULL) { + phosh_async_error_warn (err, "Failed to get hostname1 proxy"); + return; + } + g_return_if_fail (PHOSH_IS_MODE_MANAGER (self)); + self->proxy = proxy; + + g_debug ("Hostname1 interface initialized"); + g_signal_connect_object (self->proxy, + "notify::chassis", + G_CALLBACK (on_chassis_changed), + self, + G_CONNECT_SWAPPED); + on_chassis_changed (self, NULL, self->proxy); + + wl = phosh_wayland_get_default (); + g_signal_connect_object (wl, + "notify::seat-capabilities", + G_CALLBACK (on_seat_capabilities_changed), + self, + G_CONNECT_SWAPPED); + on_seat_capabilities_changed (self, NULL, wl); + + g_signal_connect_object (self->monitor_manager, + "notify::n-monitors", + G_CALLBACK (on_n_monitors_changed), + self, + G_CONNECT_SWAPPED); + /* n_monitors is always updated in update_props () */ +} + + +static void +phosh_mode_manager_idle_init (PhoshManager *manager) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (manager); + + phosh_dbus_hostname1_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + BUS_NAME, + OBJECT_PATH, + self->cancel, + (GAsyncReadyCallback) on_proxy_new_for_bus_finish, + self); +} + +static void +phosh_mode_manager_constructed (GObject *object) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (object); + + G_OBJECT_CLASS (phosh_mode_manager_parent_class)->constructed (object); + + self->monitor_manager = phosh_shell_get_monitor_manager (phosh_shell_get_default ()); +} + + +static void +phosh_mode_manager_dispose (GObject *object) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (object); + + g_clear_pointer (&self->tablet_mode_switch, zphoc_tablet_mode_switch_v1_destroy); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->proxy); + + G_OBJECT_CLASS (phosh_mode_manager_parent_class)->dispose (object); +} + + +static void +phosh_mode_manager_finalize (GObject *object) +{ + PhoshModeManager *self = PHOSH_MODE_MANAGER (object); + + g_clear_pointer (&self->chassis, g_free); + + G_OBJECT_CLASS (phosh_mode_manager_parent_class)->finalize (object); +} + + +static void +phosh_mode_manager_class_init (PhoshModeManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->constructed = phosh_mode_manager_constructed; + object_class->dispose = phosh_mode_manager_dispose; + object_class->finalize = phosh_mode_manager_finalize; + object_class->get_property = phosh_mode_manager_get_property; + + manager_class->idle_init = phosh_mode_manager_idle_init; + + props[PROP_DEVICE_TYPE] = + g_param_spec_enum ("device-type", + "Device Type", + "The device type", + PHOSH_TYPE_MODE_DEVICE_TYPE, + PHOSH_MODE_DEVICE_TYPE_PHONE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + props[PROP_HW_FLAGS] = + g_param_spec_flags ("hw-flags", + "Hardware flags", + "Flags for available hardware", + PHOSH_TYPE_MODE_HW_FLAGS, + PHOSH_MODE_HW_NONE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + /** + * PhoshMode:device-mimicry: + * + * What this device plus external hardware should be handled + * like. E.g. a phone with keyboard and mouse and 2nd screen looks + * much like a desktop. A touch laptop with removable keyboard can + * look like a tablet. + */ + props[PROP_MIMICRY] = + g_param_spec_enum ("mimicry", + "Device Mimicry", + "The device mimicry", + PHOSH_TYPE_MODE_DEVICE_TYPE, + PHOSH_MODE_DEVICE_TYPE_PHONE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_mode_manager_init (PhoshModeManager *self) +{ + self->hw_flags = PHOSH_MODE_HW_NONE; + self->device_type = PHOSH_MODE_DEVICE_TYPE_UNKNOWN; + self->mimicry = PHOSH_MODE_DEVICE_TYPE_UNKNOWN; + self->cancel = g_cancellable_new (); + self->is_tablet_mode = -1; +} + + +PhoshModeManager * +phosh_mode_manager_new (void) +{ + return PHOSH_MODE_MANAGER (g_object_new (PHOSH_TYPE_MODE_MANAGER, NULL)); +} + + +PhoshModeDeviceType +phosh_mode_manager_get_device_type (PhoshModeManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MODE_MANAGER (self), PHOSH_MODE_DEVICE_TYPE_UNKNOWN); + + return self->device_type; +} + + +PhoshModeDeviceType +phosh_mode_manager_get_mimicry (PhoshModeManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MODE_MANAGER (self), PHOSH_MODE_DEVICE_TYPE_UNKNOWN); + + return self->mimicry; +} diff --git a/src/mode-manager.h b/src/mode-manager.h new file mode 100644 index 000000000..9a70e78f1 --- /dev/null +++ b/src/mode-manager.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include + + +G_BEGIN_DECLS + +/** + * PhoshModeDeviceType: + * @PHOSH_MODE_DEVICE_TYPE_UNKNOWN: unknown device type + * @PHOSH_MODE_DEVICE_TYPE_PHONE: a phone/handset + * @PHOSH_MODE_DEVICE_TYPE_LAPTOP: a laptop + * @PHOSH_MODE_DEVICE_TYPE_DESKTOP: a desktop computer + * @PHOSH_MODE_DEVICE_TYPE_TABLET: a tablet computer + * @PHOSH_MODE_DEVICE_TYPE_CONVERTIBLE: a convertible + * @PHOSH_MODE_DEVICE_TYPE_EMBEDDED: an embedded device + * + * A type of device + */ +typedef enum { + PHOSH_MODE_DEVICE_TYPE_UNKNOWN, + PHOSH_MODE_DEVICE_TYPE_PHONE, + PHOSH_MODE_DEVICE_TYPE_LAPTOP, + PHOSH_MODE_DEVICE_TYPE_DESKTOP, + PHOSH_MODE_DEVICE_TYPE_TABLET, + PHOSH_MODE_DEVICE_TYPE_CONVERTIBLE, + PHOSH_MODE_DEVICE_TYPE_EMBEDDED, +} PhoshModeDeviceType; + +/** + * PhoshModeHwFlags: + * @PHOSH_MODE_HW_NONE: nothing + * @PHOSH_MODE_HW_EXT_DISPLAY: external display + * @PHOSH_MODE_HW_KEYBOARD: keyboard + * @PHOSH_MODE_HW_POINTER: pointing device + * + * Attached external hardware + */ +typedef enum { + PHOSH_MODE_HW_NONE = 0, + PHOSH_MODE_HW_EXT_DISPLAY = (1 << 1), + PHOSH_MODE_HW_KEYBOARD = (1 << 2), + PHOSH_MODE_HW_POINTER = (1 << 3), +} PhoshModeHwFlags; + +/* TODO: Use phoc-device-state for keyboard detection */ +#define PHOSH_MODE_DOCKED_PHONE_MASK (PHOSH_MODE_HW_EXT_DISPLAY | PHOSH_MODE_HW_POINTER) +#define PHOSH_MODE_DOCKED_TABLET_MASK (PHOSH_MODE_HW_POINTER) +#define PHOSH_MODE_DOCKED_EMBEDDED_MASK (PHOSH_MODE_HW_EXT_DISPLAY | PHOSH_MODE_HW_POINTER) + +#define PHOSH_TYPE_MODE_MANAGER (phosh_mode_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshModeManager, phosh_mode_manager, PHOSH, MODE_MANAGER, PhoshManager) + +PhoshModeManager *phosh_mode_manager_new (void); +PhoshModeDeviceType phosh_mode_manager_get_device_type (PhoshModeManager *self); +PhoshModeDeviceType phosh_mode_manager_get_mimicry (PhoshModeManager *self); + +G_END_DECLS diff --git a/src/monitor-manager.c b/src/monitor-manager.c new file mode 100644 index 000000000..e7b416fac --- /dev/null +++ b/src/monitor-manager.c @@ -0,0 +1,1709 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Somewhat based on mutter's src/backends/meta-monitor-manager.c + */ + +#define G_LOG_DOMAIN "phosh-monitor-manager" + +#include "monitor-manager.h" +#include "monitor/head-priv.h" +#include "monitor/monitor.h" + +#include "wlr-gamma-control-unstable-v1-client-protocol.h" +#include "phosh-wayland.h" +#include "shell-priv.h" + +#include "util.h" + +#include "dbus/gsd-color-dbus.h" + +#include + +#define GSD_COLOR_BUS_NAME "org.gnome.SettingsDaemon.Color" +#define GSD_COLOR_OBJECT_PATH "/org/gnome/SettingsDaemon/Color" + +/** + * PhoshMonitorManager: + * + * The singleton that manages available monitors + * + * This keeps track of all monitors and handles the + * org.gnome.Mutter.DisplayConfig DBus interface via + * #PhoshDBusDisplayConfig. This includes individual monitor + * configuration as well as blanking/power saving. + */ + +/* Equivalent to the 'layout-mode' enum in org.gnome.Mutter.DisplayConfig */ +typedef enum PhoshMonitorMAnagerLayoutMode { + PHOSH_MONITOR_MANAGER_LAYOUT_MODE_LOGICAL = 1, + PHOSH_MONITOR_MANAGER_LAYOUT_MODE_PHYSICAL = 2 +} PhoshMonitorManagerLayoutMode; + +enum { + PROP_0, + PROP_SENSOR_PROXY_MANAGER, + PROP_N_MONITORS, + PROP_NIGHT_LIGHT_TEMP, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + SIGNAL_MONITOR_ADDED, + SIGNAL_MONITOR_REMOVED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +static void phosh_monitor_manager_display_config_init ( + PhoshDBusDisplayConfigIface *iface); + +typedef struct _PhoshMonitorManager +{ + PhoshDBusDisplayConfigSkeleton parent; + + PhoshSensorProxyManager *sensor_proxy_manager; + GBinding *sensor_proxy_binding; + + PhoshDBusColor *gsd_color_proxy; + guint32 night_light_temp; + + GPtrArray *monitors; /* Currently known monitors */ + GPtrArray *heads; /* Currently known heads */ + + int dbus_name_id; + int serial; + + PhoshHead *pending_primary; + uint32_t zwlr_output_serial; + + GCancellable *cancel; +} PhoshMonitorManager; + +G_DEFINE_TYPE_WITH_CODE (PhoshMonitorManager, + phosh_monitor_manager, + PHOSH_DBUS_TYPE_DISPLAY_CONFIG_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_DISPLAY_CONFIG, + phosh_monitor_manager_display_config_init)); + + +static const double known_diagonals[] = { + 12.1, + 13.3, + 15.6, +}; + + +static char * +diagonal_to_str (double d) +{ + unsigned int i; + + for (i = 0; i < G_N_ELEMENTS (known_diagonals); i++) { + double delta; + + delta = fabs (known_diagonals[i] - d); + if (delta < 0.1) + return g_strdup_printf ("%0.1lf\"", known_diagonals[i]); + } + + return g_strdup_printf ("%d\"", (int) (d + 0.5)); +} + + +static char* +get_display_name (PhoshHead *head) +{ + const char *vendor_name = NULL; + const char *product_name = NULL; + g_autofree char *inches = NULL; + + if (phosh_head_is_builtin (head)) + return g_strdup (_("Built-in display")); + + if (head->phys.width > 0 && head->phys.height > 0) { + double d = sqrt (head->phys.width * head->phys.width + + head->phys.height * head->phys.height); + inches = diagonal_to_str (d / 25.4); + } + + if (head->product && g_strcmp0 (head->product, "")) + product_name = head->product; + + if (head->vendor && g_strcmp0 (head->vendor, "")) + vendor_name = head->vendor; + + if (vendor_name != NULL) { + if (inches != NULL) { + return g_strdup_printf (C_("This is a monitor vendor name, followed by a " + "size in inches, like 'Dell 15\"'", + "%s %s"), + vendor_name, inches); + } + if (product_name != NULL) { + return g_strdup_printf (C_("This is a monitor vendor name followed by " + "product/model name where size in inches " + "could not be calculated, e.g. Dell U2414H", + "%s %s"), + vendor_name, product_name); + } + } + + if (head->name) + return g_strdup (head->name); + + /* Translators: An unknown monitor type */ + return g_strdup (_("Unknown")); +} + + +static PhoshHead * +phosh_monitor_manager_get_head_from_monitor (PhoshMonitorManager *self, PhoshMonitor *monitor) +{ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + if (!g_strcmp0 (monitor->name, head->name)) { + return head; + } + } + + return NULL; +} + + +/* + * DBus Interface + */ + +static gboolean +phosh_monitor_manager_handle_get_resources (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (skeleton); + GVariantBuilder crtc_builder, output_builder, mode_builder; + PhoshMonitor *primary_monitor; + PhoshHead *primary_head; + + g_debug ("DBus %s", __func__); + + if (phosh_monitor_manager_get_num_monitors (self) == 0) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No monitors found"); + return TRUE; + } + + primary_monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + if (!primary_monitor) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No primary monitor found"); + return TRUE; + } + primary_head = phosh_monitor_manager_get_head_from_monitor (self, primary_monitor); + if (!primary_head) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No primary monitor found"); + return TRUE; + } + + g_variant_builder_init (&crtc_builder, G_VARIANT_TYPE ("a(uxiiiiiuaua{sv})")); + g_variant_builder_init (&output_builder, G_VARIANT_TYPE ("a(uxiausauaua{sv})")); + g_variant_builder_init (&mode_builder, G_VARIANT_TYPE ("a(uxuudu)")); + + /* CRTCs (logical monitor) */ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + GVariantBuilder transforms; + + if (!head->enabled) { + g_debug ("Skipping disabled %s", head->name); + continue; + } + + g_variant_builder_init (&transforms, G_VARIANT_TYPE ("au")); + for (int j = 0; j <= WL_OUTPUT_TRANSFORM_FLIPPED_270; j++) + g_variant_builder_add (&transforms, "u", j); + + g_variant_builder_add (&crtc_builder, "(uxiiiiiuaua{sv})", + (guint32)i, /* ID */ + (guint64)i, /* crtc->crtc_id, */ + (gint32)head->x, + (gint32)head->y, + (gint32)head->mode->width, + (gint32)head->mode->height, + (gint32)0, /* current_mode_index, */ + (guint32)head->transform, + &transforms, + NULL /* properties */); + } + + /* outputs (physical screen) */ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + GVariantBuilder crtcs, modes, clones, properties; + gboolean is_primary; + + g_variant_builder_init (&crtcs, G_VARIANT_TYPE ("au")); + g_variant_builder_add (&crtcs, "u", i /* possible_crtc_index */); + g_variant_builder_init (&modes, G_VARIANT_TYPE ("au")); + g_variant_builder_add (&modes, "u", 0 /* mode_index */); + g_variant_builder_init (&clones, G_VARIANT_TYPE ("au")); + g_variant_builder_add (&clones, "u", -1 /* possible_clone_index */); + g_variant_builder_init (&properties, G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&properties, "{sv}", "vendor", + g_variant_new_string (head->vendor ?: "")); + g_variant_builder_add (&properties, "{sv}", "product", + g_variant_new_string (head->product ?: "")); + g_variant_builder_add (&properties, "{sv}", "width-mm", + g_variant_new_int32 (head->phys.width)); + g_variant_builder_add (&properties, "{sv}", "height-mm", + g_variant_new_int32 (head->phys.height)); + is_primary = (head == primary_head); + g_variant_builder_add (&properties, "{sv}", "primary", + g_variant_new_boolean (is_primary)); + + g_variant_builder_add (&output_builder, "(uxiausauaua{sv})", + (guint32)i, /* ID */ + (guint64)i, /* output->winsys_id, */ + (int) i, /* crtc_index, */ + &crtcs, + head->name, /* output->name */ + &modes, + &clones, + &properties); + } + + /* Don't bother setting up modes, they're ignored */ + + phosh_dbus_display_config_complete_get_resources ( + skeleton, + invocation, + self->serial, + g_variant_builder_end (&crtc_builder), + g_variant_builder_end (&output_builder), + g_variant_builder_end (&mode_builder), + 65535, /* max_screen_width */ + 65535 /* max_screen_height */ + ); + + return TRUE; +} + + +static gboolean +phosh_monitor_manager_handle_get_crtc_gamma (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation, + guint serial, + guint crtc_id) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (skeleton); + PhoshMonitor *monitor; + guint32 n_bytes = 0; + g_autoptr (GBytes) red_bytes = NULL, green_bytes = NULL, blue_bytes = NULL; + GVariant *red_v, *green_v, *blue_v; + + g_debug ("DBus call %s for crtc %d, serial %d", __func__, crtc_id, serial); + + if (serial != self->serial) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "The requested configuration is based on stale information"); + return TRUE; + } + + if (crtc_id >= self->monitors->len) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_INVALID_ARGS, + "Invalid crtc id %d", crtc_id); + return TRUE; + } + + monitor = g_ptr_array_index (self->monitors, crtc_id); + + /* All known clients using libgnome-desktop's + * gnome_rr_crtc_get_gamma only do so to get the size of the gamma + * table. So don't bother getting the real table since this is not + * supported by wlroots: https://github.com/swaywm/wlroots/pull/1059. + * Return an empty table instead. + */ + if (phosh_monitor_has_gamma (monitor)) + n_bytes = monitor->n_gamma_entries * 2; + + g_debug ("Gamma table entries: %d", monitor->n_gamma_entries); + red_bytes = g_bytes_new_take (g_malloc0 (n_bytes), n_bytes); + green_bytes = g_bytes_new_take (g_malloc0 (n_bytes), n_bytes); + blue_bytes = g_bytes_new_take (g_malloc0 (n_bytes), n_bytes); + + red_v = g_variant_new_from_bytes (G_VARIANT_TYPE ("aq"), red_bytes, TRUE); + green_v = g_variant_new_from_bytes (G_VARIANT_TYPE ("aq"), green_bytes, TRUE); + blue_v = g_variant_new_from_bytes (G_VARIANT_TYPE ("aq"), blue_bytes, TRUE); + + phosh_dbus_display_config_complete_get_crtc_gamma (skeleton, invocation, red_v, green_v, blue_v); + + return TRUE; +} + + +static gboolean +phosh_monitor_manager_handle_set_crtc_gamma (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation, + guint serial, + guint crtc_id, + GVariant *red_v, + GVariant *green_v, + GVariant *blue_v) +{ + g_debug ("DBus call %s for crtc %d, serial %d", __func__, crtc_id, serial); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "The requested is not supported anymore"); + return TRUE; +} + + +#define MODE_FORMAT "(siiddada{sv})" +#define MODES_FORMAT "a" MODE_FORMAT +#define MONITOR_SPEC_FORMAT "(ssss)" +#define MONITOR_FORMAT "(" MONITOR_SPEC_FORMAT MODES_FORMAT "a{sv})" +#define MONITORS_FORMAT "a" MONITOR_FORMAT + +#define LOGICAL_MONITOR_MONITORS_FORMAT "a" MONITOR_SPEC_FORMAT +#define LOGICAL_MONITOR_FORMAT "(iidub" LOGICAL_MONITOR_MONITORS_FORMAT "a{sv})" +#define LOGICAL_MONITORS_FORMAT "a" LOGICAL_MONITOR_FORMAT + + +static void +build_mode (GVariantBuilder *modes_builder, PhoshHeadMode *mode, gboolean is_current) +{ + double scale = 1.0; + GVariantBuilder supported_scales_builder, mode_properties_builder; + const char *name = "default"; + int n; + g_autofree float *scales = NULL; + + if (mode->name) + name = mode->name; + else if (!is_current) { + g_warning ("Skipping unnamend mode %p", mode); + return; + } + + g_variant_builder_init (&supported_scales_builder, G_VARIANT_TYPE ("ad")); + scales = phosh_util_calculate_supported_mode_scales (mode->width, mode->height, &n, TRUE); + for (int l = 0; l < n; l++) { + g_variant_builder_add (&supported_scales_builder, "d", + (double)scales[l]); + } + + g_variant_builder_init (&mode_properties_builder, + G_VARIANT_TYPE ("a{sv}")); + g_variant_builder_add (&mode_properties_builder, "{sv}", + "is-current", + g_variant_new_boolean (is_current)); + g_variant_builder_add (&mode_properties_builder, "{sv}", + "is-preferred", + g_variant_new_boolean (mode->preferred)); + + g_variant_builder_add (modes_builder, MODE_FORMAT, + name, + (gint32)mode->width, + (gint32)mode->height, + (double)mode->refresh / 1000.0, + (double)scale, /* preferred_scale, */ + &supported_scales_builder, + &mode_properties_builder); +} + + +static gboolean +phosh_monitor_manager_handle_get_current_state (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (skeleton); + GVariantBuilder monitors_builder, logical_monitors_builder, properties_builder; + PhoshMonitor *primary_monitor; + PhoshHead *primary_head; + + g_debug ("DBus call %s", __func__); + + primary_monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + if (!primary_monitor) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No primary monitor found"); + return TRUE; + } + primary_head = phosh_monitor_manager_get_head_from_monitor (self, primary_monitor); + if (!primary_head) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "No primary monitor found"); + return TRUE; + } + + g_variant_builder_init (&monitors_builder, + G_VARIANT_TYPE (MONITORS_FORMAT)); + g_variant_builder_init (&logical_monitors_builder, + G_VARIANT_TYPE (LOGICAL_MONITORS_FORMAT)); + + /* connected physical monitors */ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + GVariantBuilder modes_builder, monitor_properties_builder; + char *display_name; + gboolean is_builtin; + + g_variant_builder_init (&modes_builder, G_VARIANT_TYPE (MODES_FORMAT)); + + /* Ensure we have at least one mode */ + if (head->modes->len == 0) + build_mode (&modes_builder, head->mode, TRUE); + + for (int k = 0; k < head->modes->len; k++) { + PhoshHeadMode *mode = g_ptr_array_index (head->modes, k); + + build_mode (&modes_builder, mode, head->mode == mode); + } + + g_variant_builder_init (&monitor_properties_builder, + G_VARIANT_TYPE ("a{sv}")); + + is_builtin = phosh_head_is_builtin (head); + g_variant_builder_add (&monitor_properties_builder, "{sv}", + "is-builtin", + g_variant_new_boolean (is_builtin)); + + display_name = get_display_name (head); + g_variant_builder_add (&monitor_properties_builder, "{sv}", + "display-name", + g_variant_new_take_string (display_name)); + + g_variant_builder_add (&monitors_builder, MONITOR_FORMAT, + head->name, /* monitor_spec->connector */ + head->vendor ?: "", /* monitor_spec->vendor, */ + head->product ?: "", /* monitor_spec->product, */ + head->serial ?: "", /* monitor_spec->serial, */ + &modes_builder, + &monitor_properties_builder); + } + + /* Current logical monitor configuration */ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + GVariantBuilder logical_monitor_monitors_builder; + gboolean is_primary; + + if (!head->enabled) + continue; + + g_variant_builder_init (&logical_monitor_monitors_builder, + G_VARIANT_TYPE (LOGICAL_MONITOR_MONITORS_FORMAT)); + g_variant_builder_add (&logical_monitor_monitors_builder, + MONITOR_SPEC_FORMAT, + head->name, /* monitor_spec->connector, */ + head->vendor ?: "", /* monitor_spec->vendor, */ + head->product ?: "", /* monitor_spec->product, */ + head->serial ?: ""); /* monitor_spec->serial, */ + + is_primary = (head == primary_head); + g_variant_builder_add (&logical_monitors_builder, + LOGICAL_MONITOR_FORMAT, + (gint32)head->x, /* logical_monitor->rect.x */ + (gint32)head->y, /* logical_monitor->rect.y */ + (double)head->scale, /* (double) logical_monitor->scale */ + (guint32)head->transform, /* logical_monitor->transform */ + is_primary, /* logical_monitor->is_primary */ + &logical_monitor_monitors_builder, + NULL); + } + + g_variant_builder_init (&properties_builder, G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (&properties_builder, "{sv}", + "layout-mode", + g_variant_new_uint32 (PHOSH_MONITOR_MANAGER_LAYOUT_MODE_LOGICAL)); + + g_variant_builder_add (&properties_builder, "{sv}", + "supports-changing-layout-mode", + g_variant_new_boolean (TRUE)); + + phosh_dbus_display_config_complete_get_current_state ( + skeleton, + invocation, + self->serial, + g_variant_builder_end (&monitors_builder), + g_variant_builder_end (&logical_monitors_builder), + g_variant_builder_end (&properties_builder)); + + return TRUE; +} + + +#undef LOGICAL_MONITORS_FORMAT +#undef LOGICAL_MONITOR_FORMAT +#undef LOGICAL_MONITOR_MONITORS_FORMAT +#undef MODES_FORMAT +#undef MODE_FORMAT +#undef MONITORS_FORMAT +#undef MONITOR_FORMAT + + +#define MONITOR_CONFIG_FORMAT "(ssa{sv})" +#define MONITOR_CONFIGS_FORMAT "a" MONITOR_CONFIG_FORMAT + +static PhoshHead * +phosh_monitor_manager_find_head (PhoshMonitorManager *self, const char *name) +{ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + if (!g_strcmp0 (head->name, name)) + return head; + } + return NULL; +} + + +static PhoshHead * +find_head_from_variant (PhoshMonitorManager *self, + GVariant *monitor_config_variant, + char **mode, + GError **err) +{ + g_autofree char *connector = NULL; + char *mode_id = NULL; + g_autoptr (GVariant) properties_variant = NULL; + GVariantIter iter; + GVariant *value; + PhoshHead *head; + char *key; + + g_return_val_if_fail (*mode == NULL, NULL); + + g_variant_get (monitor_config_variant, "(ss@a{sv})", + &connector, + &mode_id, + &properties_variant); + + head = phosh_monitor_manager_find_head (self, connector); + if (head == NULL) { + g_set_error (err, G_IO_ERROR, G_IO_ERROR_FAILED, + "Could not find monitor for connector '%s'", connector); + return NULL; + } + g_debug ("%s should have mode id %s", head->name, mode_id); + + g_variant_iter_init (&iter, properties_variant); + while (g_variant_iter_loop (&iter, "{sv}", &key, &value)) { + /* We don't bother about any properties here */ + g_debug ("Monitor '%s': prop '%s'", connector, key); + } + + *mode = mode_id; + return head; +} + + +#define LOGICAL_MONITOR_CONFIG_FORMAT "(iidub" MONITOR_CONFIGS_FORMAT ")" + +static PhoshHead * +config_head_config_from_logical_monitor_variant (PhoshMonitorManager *self, + GVariant *logical_monitor_config_variant, + GError **err) +{ + int x, y; + unsigned int transform; + double scale; + gboolean is_primary; + PhoshHead *head = NULL; + g_autoptr (GVariantIter) monitor_configs_iter = NULL; + + g_variant_get (logical_monitor_config_variant, LOGICAL_MONITOR_CONFIG_FORMAT, + &x, + &y, + &scale, + &transform, + &is_primary, + &monitor_configs_iter); + + while (TRUE) { + PhoshHeadMode *mode; + g_autofree char *mode_name = NULL; + g_autoptr (GVariant) monitor_config_variant = + g_variant_iter_next_value (monitor_configs_iter); + + if (!monitor_config_variant) + break; + + head = find_head_from_variant (self, + monitor_config_variant, + &mode_name, + err); + if (head == NULL) + break; + + g_debug ("Head %s, mode %s in logical monitor config %p", + head->name, + mode_name, + logical_monitor_config_variant); + + mode = phosh_head_find_mode_by_name (head, mode_name); + if (mode) + head->pending.mode = mode; + else + g_warning ("Mode %s not found on head %s", mode_name, head->name); + head->pending.scale = scale; + head->pending.x = x; + head->pending.y = y; + head->pending.transform = transform; + head->pending.enabled = TRUE; + head->pending.seen = TRUE; + } + + return is_primary ? head : NULL; +} + +#undef LOGICAL_MONITOR_CONFIG_FORMAT +#undef MONITOR_CONFIGS_FORMAT +#undef MONITOR_CONFIG_FORMAT + + +static void +phosh_monitor_manager_clear_pending (PhoshMonitorManager *self) +{ + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + phosh_head_clear_pending (head); + } + + self->pending_primary = NULL; +} + + +static gboolean +phosh_monitor_manager_handle_apply_monitors_config (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation, + guint serial, + guint method, + GVariant *logical_monitor_configs_variant, + GVariant *properties_variant) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (skeleton); + GVariantIter logical_monitor_configs_iter; + GError *err = NULL; + PhoshHead *primary_head = NULL; + GVariant *layout_mode_variant = NULL; + PhoshMonitorManagerLayoutMode layout_mode = PHOSH_MONITOR_MANAGER_LAYOUT_MODE_LOGICAL; + int n_monitors = 0; + + g_debug ("DBus call %s, method: %d", __func__, method); + + if (serial != self->serial) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "The requested configuration is based on stale information"); + return TRUE; + } + + if (phosh_shell_get_locked (phosh_shell_get_default ())) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Can't configure outputs when locked"); + return TRUE; + } + + if (properties_variant) { + layout_mode_variant = g_variant_lookup_value (properties_variant, + "layout-mode", + G_VARIANT_TYPE ("u")); + if (layout_mode_variant) { + g_variant_get (layout_mode_variant, "u", &layout_mode); + g_debug ("Requested layout mode %d", layout_mode); + } + } + + if (layout_mode != PHOSH_MONITOR_MANAGER_LAYOUT_MODE_LOGICAL) { + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid layout mode specified"); + return TRUE; + } + + + /* Make sure we refresh only heads from this config run */ + phosh_monitor_manager_clear_pending (self); + + g_variant_iter_init (&logical_monitor_configs_iter, + logical_monitor_configs_variant); + + while (TRUE) { + PhoshHead *head; + + g_autoptr (GVariant) logical_monitor_config_variant = + g_variant_iter_next_value (&logical_monitor_configs_iter); + + if (!logical_monitor_config_variant) + break; + + head = config_head_config_from_logical_monitor_variant (self, + logical_monitor_config_variant, + &err); + + if (head == NULL && err) { + phosh_monitor_manager_clear_pending (self); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "%s", err->message); + return TRUE; + } + + if (head) { + if (primary_head) { + phosh_monitor_manager_clear_pending (self); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Only one primary monitor possible"); + return TRUE; + } else { + primary_head = head; + } + } + n_monitors++; + } + + if (n_monitors == 0) { + phosh_monitor_manager_clear_pending (self); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Monitor config empty"); + return TRUE; + } + + if (primary_head == NULL) { + phosh_monitor_manager_clear_pending (self); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_ACCESS_DENIED, + "Invalid primary monitor"); + return TRUE; + } + + if (method == PHOSH_MONITOR_MANAGER_CONFIG_METHOD_PERSISTENT) { + PhoshMonitor *primary_monitor; + + primary_monitor = phosh_monitor_manager_find_monitor (self, primary_head->name); + /* If the primary monitor is in the list of enabled heads we can assign it right away */ + if (primary_monitor) { + PhoshShell *shell = phosh_shell_get_default (); + + if (primary_monitor != phosh_shell_get_primary_monitor (shell)) { + g_debug ("New primary monitor is %s", primary_monitor->name); + phosh_shell_set_primary_monitor (shell, primary_monitor); + } + } else { + /* Delay primary monitor reconfiguration in case the correspdonding head + is currently disabled */ + self->pending_primary = primary_head; + } + + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + + /* If head was not seen during this invocation, disable it */ + if (!head->pending.seen) + head->pending.enabled = FALSE; + + head->pending.seen = FALSE; + } + + /* Apply new config */ + phosh_monitor_manager_apply_monitor_config (self); + } + + phosh_dbus_display_config_complete_apply_monitors_config ( + skeleton, + invocation); + + return TRUE; +} + + +static gboolean +phosh_monitor_manager_handle_set_output_ctm (PhoshDBusDisplayConfig *skeleton, + GDBusMethodInvocation *invocation, + guint serial, + guint output, + GVariant *ctm) +{ + g_debug ("Unimplemented DBus call %s", __func__); + g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR, + G_DBUS_ERROR_NOT_SUPPORTED, + "Changing the color transformation" + "matrix not supported"); + return TRUE; +} + + +static void +phosh_monitor_manager_display_config_init (PhoshDBusDisplayConfigIface *iface) +{ + iface->handle_get_resources = phosh_monitor_manager_handle_get_resources; + iface->handle_get_crtc_gamma = phosh_monitor_manager_handle_get_crtc_gamma; + iface->handle_set_crtc_gamma = phosh_monitor_manager_handle_set_crtc_gamma; + iface->handle_get_current_state = phosh_monitor_manager_handle_get_current_state; + iface->handle_apply_monitors_config = phosh_monitor_manager_handle_apply_monitors_config; + iface->handle_set_output_ctm = phosh_monitor_manager_handle_set_output_ctm; +} + + +static void +power_save_mode_changed_cb (PhoshMonitorManager *self, + GParamSpec *pspec, + gpointer user_data) +{ + int mode; + PhoshMonitorPowerSaveMode ps_mode; + + mode = phosh_dbus_display_config_get_power_save_mode ( + PHOSH_DBUS_DISPLAY_CONFIG (self)); + g_debug ("Power save mode %d requested", mode); + + switch (mode) { + case 0: + ps_mode = PHOSH_MONITOR_POWER_SAVE_MODE_ON; + break; + default: + ps_mode = PHOSH_MONITOR_POWER_SAVE_MODE_OFF; + break; + } + + phosh_monitor_manager_set_power_save_mode (self, ps_mode); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Acquired name %s", name); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (user_data); + + /* We need to use Mutter's object path here to make gnome-settings happy */ + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/gnome/Mutter/DisplayConfig", + NULL); +} + +/* + * wl_output wayland protocol + */ + +static PhoshMonitor * +find_monitor_by_wl_output (PhoshMonitorManager *self, struct wl_output *output) +{ + for (int i = 0; i < self->monitors->len; i++) { + PhoshMonitor *monitor = g_ptr_array_index (self->monitors, i); + if (monitor->wl_output == output) + return monitor; + } + return NULL; +} + + +static void +phosh_monitor_manager_set_night_light_supported (PhoshMonitorManager *self) +{ + gboolean night_light_supported = FALSE; + + for (guint i = 0; i < self->monitors->len; i++) { + PhoshMonitor *monitor = g_ptr_array_index (self->monitors, i); + + if (phosh_monitor_has_gamma (monitor)) { + night_light_supported = TRUE; + break; + } + } + + phosh_dbus_display_config_set_night_light_supported (PHOSH_DBUS_DISPLAY_CONFIG (self), + night_light_supported); +} + + +static void +on_monitor_n_gamma_entries_changed (PhoshMonitorManager *self) +{ + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + phosh_monitor_manager_set_night_light_supported (self); +} + + +static void +on_monitor_configured (PhoshMonitorManager *self, PhoshMonitor *monitor) +{ + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + g_signal_emit (self, signals[SIGNAL_MONITOR_ADDED], 0, monitor); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_MONITORS]); + + g_signal_handlers_disconnect_by_data (monitor, self); + + /* A monitor reconfiguration via DBus might have a pending primary monitor change */ + if (self->pending_primary) { + PhoshMonitor *primary_monitor; + PhoshShell *shell = phosh_shell_get_default (); + + primary_monitor = phosh_monitor_manager_find_monitor (self, self->pending_primary->name); + if (primary_monitor == monitor) { + g_warning ("New primary monitor %s", primary_monitor->name); + phosh_shell_set_primary_monitor (shell, primary_monitor); + self->pending_primary = NULL; + } + } + + g_signal_connect_swapped (monitor, "notify::n-gamma-entries", + G_CALLBACK (on_monitor_n_gamma_entries_changed), + self); + + phosh_monitor_manager_set_night_light_supported (self); + + /* Update night light */ + if (self->night_light_temp > 0 && phosh_monitor_has_gamma (monitor)) + phosh_monitor_set_color_temp (monitor, self->night_light_temp); +} + + +static void +on_monitor_removed (PhoshMonitorManager *self, + PhoshMonitor *monitor, + gpointer *data) +{ + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + g_debug ("Monitor %p (%s) removed", monitor, monitor->name); + g_ptr_array_remove (self->monitors, monitor); + phosh_monitor_manager_set_night_light_supported (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_MONITORS]); +} + + +static void +phosh_monitor_manager_add_monitor (PhoshMonitorManager *self, PhoshMonitor *monitor) +{ + g_ptr_array_add (self->monitors, monitor); + /* Delay emission of 'monitor-added' until it's configured */ + g_signal_connect_swapped (monitor, + "configured", + G_CALLBACK (on_monitor_configured), + self); +} + + +static void +on_wl_outputs_changed (PhoshMonitorManager *self, GParamSpec *pspec, PhoshWayland *wl) +{ + GHashTable *wl_outputs = phosh_wayland_get_wl_outputs (wl); + GHashTableIter iter; + struct wl_output *wl_output; + PhoshMonitor *monitor; + + /* Check for gone outputs */ + for (int i = 0; i < self->monitors->len; i++) { + monitor = g_ptr_array_index (self->monitors, i); + if (!phosh_wayland_has_wl_output (wl, monitor->wl_output)) { + g_debug ("Monitor %p (%s) gone", monitor, monitor->name); + /* Give up on wayland resources early otherwise we might not be able to + bind them if the same output shows up again */ + g_object_run_dispose (G_OBJECT (monitor)); + g_signal_emit (self, signals[SIGNAL_MONITOR_REMOVED], 0, monitor); + /* The monitor is removed from monitors in the class'es default + * signal handler */ + return; + } + } + + /* Check for new outputs */ + g_hash_table_iter_init (&iter, wl_outputs); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&wl_output)) { + if (!find_monitor_by_wl_output (self, wl_output)) { + monitor = phosh_monitor_new_from_wl_output (wl_output); + phosh_monitor_manager_add_monitor (self, monitor); + g_debug ("Monitor %p added", monitor); + return; + } + } +} + + +/* + * wlr_output_manager wayland protocol + */ + +static void +on_head_finished (PhoshMonitorManager *self, + PhoshHead *head) +{ + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + if (g_ptr_array_remove (self->heads, head)) + g_debug ("Removing head %p", head); + else + g_warning ("Tried to remove inexistend head %p", head); + + phosh_dbus_display_config_emit_monitors_changed (PHOSH_DBUS_DISPLAY_CONFIG (self)); +} + + +static void +zwlr_output_manager_v1_handle_head (void *data, + struct zwlr_output_manager_v1 *manager, + struct zwlr_output_head_v1 *wlr_head) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (data); + PhoshHead *head; + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + head = phosh_head_new_from_wlr_head (wlr_head); + g_debug ("New head %p", head); + g_ptr_array_add (self->heads, head); + g_signal_connect_swapped (head, "head-finished", G_CALLBACK (on_head_finished), self); + + phosh_dbus_display_config_emit_monitors_changed (PHOSH_DBUS_DISPLAY_CONFIG (self)); +} + + +static void +zwlr_output_manager_v1_handle_done (void *data, + struct zwlr_output_manager_v1 *manager, + uint32_t serial) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (data); + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + g_debug ("Got zwlr_output_manager serial %u", serial); + self->zwlr_output_serial = serial; + self->serial++; + + phosh_dbus_display_config_emit_monitors_changed (PHOSH_DBUS_DISPLAY_CONFIG (self)); +} + + +static const struct zwlr_output_manager_v1_listener zwlr_output_manager_v1_listener = { + .head = zwlr_output_manager_v1_handle_head, + .done = zwlr_output_manager_v1_handle_done, +}; + + +static void +zwlr_output_configuration_v1_handle_succeeded (void *data, + struct zwlr_output_configuration_v1 *config) +{ + g_debug ("New output configuration %p applied", config); + zwlr_output_configuration_v1_destroy (config); +} + + +static void +zwlr_output_configuration_v1_handle_failed (void *data, + struct zwlr_output_configuration_v1 *config) +{ + /* TODO: bubble up error */ + g_warning ("Failed to apply New output %p configuration", config); + zwlr_output_configuration_v1_destroy (config); +} + + +static void +zwlr_output_configuration_v1_handle_cancelled (void *data, + struct zwlr_output_configuration_v1 *config) +{ + zwlr_output_configuration_v1_destroy (config); + g_warning ("Failed to apply New output configuration %p due to changes", config); +} + + +static const struct zwlr_output_configuration_v1_listener config_listener = { + .succeeded = zwlr_output_configuration_v1_handle_succeeded, + .failed = zwlr_output_configuration_v1_handle_failed, + .cancelled = zwlr_output_configuration_v1_handle_cancelled, +}; + + +static void +phosh_monitor_manager_dispose (GObject *object) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (object); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_object (&self->sensor_proxy_manager); + g_clear_pointer (&self->sensor_proxy_binding, g_binding_unbind); + + g_clear_object (&self->gsd_color_proxy); + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + G_OBJECT_CLASS (phosh_monitor_manager_parent_class)->dispose (object); +} + + +static void +phosh_monitor_manager_finalize (GObject *object) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (object); + + g_ptr_array_free (self->monitors, TRUE); + g_ptr_array_free (self->heads, TRUE); + + G_OBJECT_CLASS (phosh_monitor_manager_parent_class)->finalize (object); +} + +/* + * PhoshMonitorManager Class + */ + +static void +phosh_monitor_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + phosh_monitor_manager_set_sensor_proxy_manager (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_monitor_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + g_value_set_object (value, self->sensor_proxy_manager); + break; + case PROP_N_MONITORS: + g_value_set_int (value, self->monitors->len); + break; + case PROP_NIGHT_LIGHT_TEMP: + g_value_set_uint (value, self->night_light_temp); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_gsd_color_temperature_changed (PhoshMonitorManager*self) +{ + guint32 temp; + + temp = phosh_dbus_color_get_temperature (self->gsd_color_proxy); + + if (temp == self->night_light_temp) + return; + + self->night_light_temp = temp; + + g_return_if_fail (self->night_light_temp > 0); + g_debug ("Setting night light: %dK", self->night_light_temp); + for (int i = 0; i < self->monitors->len; i++) { + gboolean success; + PhoshMonitor *monitor = g_ptr_array_index (self->monitors, i); + + success = phosh_monitor_set_color_temp (monitor, self->night_light_temp); + if (!success) + g_warning ("Failed to set gamma for %s", monitor->name); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NIGHT_LIGHT_TEMP]); +} + + +static void +on_gsd_color_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + gpointer user_data) + +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (user_data); + g_autoptr (GError) err = NULL; + PhoshDBusColor *proxy; + + proxy = phosh_dbus_color_proxy_new_for_bus_finish (res, &err); + + if (!proxy) { + phosh_async_error_warn (err, "Failed to get gsd color proxy"); + return; + } + + self->gsd_color_proxy = PHOSH_DBUS_COLOR (proxy); + g_signal_connect_object (self->gsd_color_proxy, + "notify::temperature", + G_CALLBACK (on_gsd_color_temperature_changed), + self, + G_CONNECT_SWAPPED); + on_gsd_color_temperature_changed (self); + + g_debug ("GSD Color initialized"); +} + + +static gboolean +on_idle (PhoshMonitorManager *self) +{ + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.gnome.Mutter.DisplayConfig", + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); + + phosh_dbus_color_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + GSD_COLOR_BUS_NAME, + GSD_COLOR_OBJECT_PATH, + self->cancel, + on_gsd_color_proxy_new_for_bus_finish, + self); + + return FALSE; +} + + +static void +phosh_monitor_manager_constructed (GObject *object) +{ + PhoshMonitorManager *self = PHOSH_MONITOR_MANAGER (object); + PhoshWayland *wl = phosh_wayland_get_default (); + GHashTableIter iter; + struct wl_output *wl_output; + struct zwlr_output_manager_v1 *zwlr_output_manager_v1; + guint id; + + G_OBJECT_CLASS (phosh_monitor_manager_parent_class)->constructed (object); + + g_signal_connect (self, "notify::power-save-mode", + G_CALLBACK (power_save_mode_changed_cb), NULL); + + g_signal_connect_swapped (phosh_wayland_get_default (), + "notify::wl-outputs", + G_CALLBACK (on_wl_outputs_changed), + self); + /* Get initial output list */ + g_hash_table_iter_init (&iter, phosh_wayland_get_wl_outputs (wl)); + while (g_hash_table_iter_next (&iter, NULL, (gpointer)&wl_output)) { + PhoshMonitor *monitor = phosh_monitor_new_from_wl_output (wl_output); + phosh_monitor_manager_add_monitor (self, monitor); + } + + zwlr_output_manager_v1 = phosh_wayland_get_zwlr_output_manager_v1 (wl); + zwlr_output_manager_v1_add_listener (zwlr_output_manager_v1, + &zwlr_output_manager_v1_listener, + self); + + phosh_dbus_display_config_set_apply_monitors_config_allowed ( + PHOSH_DBUS_DISPLAY_CONFIG (self), TRUE); + + id = g_idle_add ((GSourceFunc) on_idle, self); + g_source_set_name_by_id (id, "[PhoshMonitorManager] idle"); +} + + +static void +phosh_monitor_manager_class_init (PhoshMonitorManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_monitor_manager_constructed; + object_class->dispose = phosh_monitor_manager_dispose; + object_class->finalize = phosh_monitor_manager_finalize; + object_class->get_property = phosh_monitor_manager_get_property; + object_class->set_property = phosh_monitor_manager_set_property; + + props[PROP_SENSOR_PROXY_MANAGER] = + g_param_spec_object ("sensor-proxy-manager", "", "", + PHOSH_TYPE_SENSOR_PROXY_MANAGER, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshMonitorManager:n-monitors: + * + * The number of currently enabled monitors + */ + props[PROP_N_MONITORS] = + g_param_spec_int ("n-monitors", "", "", + 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshMonitorManager:night-light-temp: + * + * Night light color temperature + */ + props[PROP_NIGHT_LIGHT_TEMP] = + g_param_spec_uint ("night-light-temp", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshMonitorManager::monitor-added: + * @manager: The #PhoshMonitorManager emitting the signal. + * @monitor: The #PhoshMonitor being added. + * + * Emitted whenever a monitor was added. + */ + signals[SIGNAL_MONITOR_ADDED] = g_signal_new ( + "monitor-added", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, PHOSH_TYPE_MONITOR); + + /** + * PhoshMonitorManager::monitor-removed: + * @manager: The #PhoshMonitorManager emitting the signal. + * @monitor: The #PhoshMonitor being removed. + * + * Emitted whenever a monitor is about to be removed. + */ + signals[SIGNAL_MONITOR_REMOVED] = g_signal_new_class_handler ( + "monitor-removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_CLEANUP, + G_CALLBACK (on_monitor_removed), + NULL, NULL, NULL, G_TYPE_NONE, 1, PHOSH_TYPE_MONITOR); +} + +static void +phosh_monitor_manager_init (PhoshMonitorManager *self) +{ + self->monitors = g_ptr_array_new_with_free_func ((GDestroyNotify) (g_object_unref)); + self->heads = g_ptr_array_new_with_free_func ((GDestroyNotify) (g_object_unref)); + self->serial = 1; + + self->cancel = g_cancellable_new (); +} + + +PhoshMonitorManager * +phosh_monitor_manager_new (PhoshSensorProxyManager *proxy) +{ + return g_object_new (PHOSH_TYPE_MONITOR_MANAGER, + "sensor-proxy-manager", proxy, + NULL); +} + +/** + * phosh_monitor_manager_get_monitor: + * @self: The monitor manager + * @num: The number of the monitor to get + * + * Get the nth monitor in the list of known monitors + * + * Returns:(nullable)(transfer none): The monitor + */ +PhoshMonitor * +phosh_monitor_manager_get_monitor (PhoshMonitorManager *self, guint num) +{ + if (num >= self->monitors->len) + return NULL; + + return g_ptr_array_index (self->monitors, num); +} + +/** + * phosh_monitor_manager_find_monitor: + * @self: The monitor manager + * @name: The name of the monitor to find + * + * Find a monitor by its name + * + * Returns:(nullable)(transfer none): The monitor if found, otherwise %NULL + */ +PhoshMonitor * +phosh_monitor_manager_find_monitor (PhoshMonitorManager *self, const char *name) +{ + for (int i = 0; i < self->monitors->len; i++) { + PhoshMonitor *monitor = g_ptr_array_index (self->monitors, i); + if (!g_strcmp0 (monitor->name, name)) + return monitor; + } + return NULL; +} + + +guint +phosh_monitor_manager_get_num_monitors (PhoshMonitorManager *self) +{ + return self->monitors->len; +} + +/** + * phosh_monitor_manager_set_monitor_transform: + * @self: A #PhoshMonitor + * @monitor: The #PhoshMonitor to set the transform on + * @transform: The #PhoshMonitorTransform to set + * + * Sets monitor's transform. This will become active after the next + * call to #phosh_monitor_manager_apply_monitor_config(). + * + * If necessary other heads will be moved to avoid gaps and + * overlapping heads in the layout. + */ +void +phosh_monitor_manager_set_monitor_transform (PhoshMonitorManager *self, + PhoshMonitor *monitor, + PhoshMonitorTransform transform) +{ + PhoshHead *head; + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + g_return_if_fail (phosh_monitor_is_configured (monitor)); + head = phosh_monitor_manager_get_head_from_monitor (self, monitor); + g_return_if_fail (PHOSH_IS_HEAD (head)); + + phosh_head_set_pending_transform (head, transform, self->heads); +} + +/** + * phosh_monitor_manager_set_monitor_scale: + * @self: A #PhoshMonitor + * @monitor: The #PhoshMonitor to set the scale for + * @scale: The scale to set + * + * Sets monitor's scale. This will become active after the next + * call to #phosh_monitor_manager_apply_monitor_config(). + */ +void +phosh_monitor_manager_set_monitor_scale (PhoshMonitorManager *self, + PhoshMonitor *monitor, + double scale) +{ + PhoshHead *head; + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + g_return_if_fail (phosh_monitor_is_configured (monitor)); + head = phosh_monitor_manager_get_head_from_monitor (self, monitor); + g_return_if_fail (PHOSH_IS_HEAD (head)); + + phosh_head_set_pending_scale (head, scale, self->heads); +} + +/** + * phosh_monitor_manager_apply_monitor_config + * @self: a #PhoshMonitorManager + * + * Applies a full output configuration + */ +void +phosh_monitor_manager_apply_monitor_config (PhoshMonitorManager *self) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + struct zwlr_output_configuration_v1 *config; + struct zwlr_output_manager_v1 *output_manager = + phosh_wayland_get_zwlr_output_manager_v1 (wl); + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + config = zwlr_output_manager_v1_create_configuration (output_manager, + self->zwlr_output_serial); + + zwlr_output_configuration_v1_add_listener (config, &config_listener, self); + for (int i = 0; i < self->heads->len; i++) { + struct zwlr_output_configuration_head_v1 *config_head; + PhoshHead *head = g_ptr_array_index (self->heads, i); + struct zwlr_output_head_v1 *wlr_head = phosh_head_get_wlr_head (head); + struct zwlr_output_mode_v1 *wlr_mode; + + g_debug ("Adding %sabled head %s to configuration", + head->pending.enabled ? "en" : "dis", + head->name); + + if (!head->pending.enabled) { + zwlr_output_configuration_v1_disable_head (config, wlr_head); + continue; + } + + config_head = zwlr_output_configuration_v1_enable_head (config, wlr_head); + + /* A disabled head might not have a mode when set */ + if (head->pending.mode) { + wlr_mode = head->pending.mode->wlr_mode; + } else { + wlr_mode = phosh_head_get_preferred_mode (head)->wlr_mode; + } + + zwlr_output_configuration_head_v1_set_mode (config_head, wlr_mode); + zwlr_output_configuration_head_v1_set_position (config_head, + head->pending.x, head->pending.y); + zwlr_output_configuration_head_v1_set_transform (config_head, head->pending.transform); + zwlr_output_configuration_head_v1_set_scale (config_head, + wl_fixed_from_double (head->pending.scale)); + } + + zwlr_output_configuration_v1_apply (config); +} + +void +phosh_monitor_manager_set_sensor_proxy_manager (PhoshMonitorManager *self, + PhoshSensorProxyManager *manager) +{ + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (manager) || manager == NULL); + + g_clear_object (&self->sensor_proxy_manager); + g_clear_pointer (&self->sensor_proxy_binding, g_binding_unbind); + + if (manager == NULL) + return; + + self->sensor_proxy_manager = g_object_ref (manager); + self->sensor_proxy_binding = g_object_bind_property (manager, "has-accelerometer", + self, "panel-orientation-managed", + G_BINDING_SYNC_CREATE); +} + +/** + * phosh_monitor_manager_set_power_save_mode + * @self: a #PhoshMonitorManager + * @mode: The power save mode to set + * + * Applies a power save mode to all monitors + */ +void +phosh_monitor_manager_set_power_save_mode (PhoshMonitorManager *self, + PhoshMonitorPowerSaveMode mode) +{ + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (self)); + + for (int i = 0; i < self->monitors->len; i++) { + PhoshMonitor *monitor = g_ptr_array_index (self->monitors, i); + + phosh_monitor_set_power_save_mode (monitor, mode); + } +} + +/** + * phosh_monitor_manager_enable_fallback: + * @self: a #PhoshMonitorManager + * + * When all heads are disabled look for a fallback to enable. This can be useful + * when e.g. only external display is enabled and that gets unplugged. + * + * Returns: %TRUE if a new head was enabled, %FALSE otherwise + */ +gboolean +phosh_monitor_manager_enable_fallback (PhoshMonitorManager *self) +{ + PhoshHead *builtin_head = NULL; + + if (!self->heads->len) + return FALSE; + + /* Make sure all display changes got processed otherwise we might try to re-enable + a just gone head */ + phosh_wayland_roundtrip (phosh_wayland_get_default ()); + + for (int i = 0; i < self->heads->len; i++) { + PhoshHead *head = g_ptr_array_index (self->heads, i); + + if (phosh_head_get_enabled (head)) { + g_warning ("%s still enabled, no fallback needed", head->name); + return FALSE; + } + + if (phosh_head_is_builtin (head) && !builtin_head) { + builtin_head = head; + } + } + + if (!builtin_head) + return FALSE; + + g_debug ("Enabling fallback head %s", builtin_head->name); + phosh_head_set_pending_enabled (builtin_head, TRUE); + phosh_monitor_manager_apply_monitor_config (self); + + return TRUE; +} + +/** + * phosh_monitor_manager_get_night_light_supported: + * @self: The monitor manager + * + * Checks if any output supports night light + * + * Returns: %TRUE if night light is supported + */ +gboolean +phosh_monitor_manager_get_night_light_supported (PhoshMonitorManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR_MANAGER (self), FALSE); + + return phosh_dbus_display_config_get_night_light_supported (PHOSH_DBUS_DISPLAY_CONFIG (self)); +} + +/** + * phosh_monitor_manager_get_night_light_temp: + * @self: The monitor manager + * + * Get the night light color temperature + * + * Returns: The night light color temperature in K + */ +guint32 +phosh_monitor_manager_get_night_light_temp (PhoshMonitorManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR_MANAGER (self), 0); + + return self->night_light_temp; +} diff --git a/src/monitor-manager.h b/src/monitor-manager.h new file mode 100644 index 000000000..f55a09bf8 --- /dev/null +++ b/src/monitor-manager.h @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include "dbus/phosh-display-dbus.h" +#include "monitor/monitor.h" + +#include "sensor-proxy-manager.h" + +#include + +G_BEGIN_DECLS + +/** + * PhoshMonitorManagerConfigMethod: + * @PHOSH_MONITOR_MANAGER_CONFIG_METHOD_VERIFY: verify the configuration + * @PHOSH_MONITOR_MANAGER_CONFIG_METHOD_TEMPORARY: configuration is temporary + * @PHOSH_MONITOR_MANAGER_CONFIG_METHOD_PERSISTENT: configuration is permanent + * + * Equivalent to the 'method' enum in org.gnome.Mutter.DisplayConfig + */ +typedef enum _MetaMonitorsConfigMethod +{ + PHOSH_MONITOR_MANAGER_CONFIG_METHOD_VERIFY = 0, + PHOSH_MONITOR_MANAGER_CONFIG_METHOD_TEMPORARY = 1, + PHOSH_MONITOR_MANAGER_CONFIG_METHOD_PERSISTENT = 2, +} PhoshMonitorManagerConfigMethod; + +#define PHOSH_TYPE_MONITOR_MANAGER (phosh_monitor_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshMonitorManager, phosh_monitor_manager, PHOSH, MONITOR_MANAGER, + PhoshDBusDisplayConfigSkeleton) + +PhoshMonitorManager * phosh_monitor_manager_new (PhoshSensorProxyManager *proxy); +PhoshMonitor * phosh_monitor_manager_get_monitor (PhoshMonitorManager *self, + guint num); +guint phosh_monitor_manager_get_num_monitors (PhoshMonitorManager *self); +PhoshMonitor * phosh_monitor_manager_find_monitor (PhoshMonitorManager *self, + const char *name); +void phosh_monitor_manager_set_monitor_transform (PhoshMonitorManager *self, + PhoshMonitor *monitor, + PhoshMonitorTransform transform); +void phosh_monitor_manager_set_monitor_scale (PhoshMonitorManager *self, + PhoshMonitor *monitor, + double scale); +void phosh_monitor_manager_apply_monitor_config (PhoshMonitorManager *self); +void phosh_monitor_manager_set_sensor_proxy_manager (PhoshMonitorManager *self, + PhoshSensorProxyManager *manager); +gboolean phosh_monitor_manager_enable_fallback (PhoshMonitorManager *self); +void phosh_monitor_manager_set_power_save_mode (PhoshMonitorManager *self, + PhoshMonitorPowerSaveMode mode); +gboolean phosh_monitor_manager_get_night_light_supported (PhoshMonitorManager *self); +guint32 phosh_monitor_manager_get_night_light_temp (PhoshMonitorManager *self); + +G_END_DECLS diff --git a/src/monitor/gamma-table.c b/src/monitor/gamma-table.c new file mode 100644 index 000000000..1b30b5704 --- /dev/null +++ b/src/monitor/gamma-table.c @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Based on wl-gammarelay Jerko Steiner which is also GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "phosh-gamma-table" + +#include "gamma-table.h" + +#include + +static const float blackbody_color[] = { + 1.00000000, 0.18172716, 0.00000000, /* 1000K */ + 1.00000000, 0.25503671, 0.00000000, /* 1100K */ + 1.00000000, 0.30942099, 0.00000000, /* 1200K */ + 1.00000000, 0.35357379, 0.00000000, /* ... */ + 1.00000000, 0.39091524, 0.00000000, + 1.00000000, 0.42322816, 0.00000000, + 1.00000000, 0.45159884, 0.00000000, + 1.00000000, 0.47675916, 0.00000000, + 1.00000000, 0.49923747, 0.00000000, + 1.00000000, 0.51943421, 0.00000000, + 1.00000000, 0.54360078, 0.08679949, + 1.00000000, 0.56618736, 0.14065513, + 1.00000000, 0.58734976, 0.18362641, + 1.00000000, 0.60724493, 0.22137978, + 1.00000000, 0.62600248, 0.25591950, + 1.00000000, 0.64373109, 0.28819679, + 1.00000000, 0.66052319, 0.31873863, + 1.00000000, 0.67645822, 0.34786758, + 1.00000000, 0.69160518, 0.37579588, + 1.00000000, 0.70602449, 0.40267128, + 1.00000000, 0.71976951, 0.42860152, + 1.00000000, 0.73288760, 0.45366838, + 1.00000000, 0.74542112, 0.47793608, + 1.00000000, 0.75740814, 0.50145662, + 1.00000000, 0.76888303, 0.52427322, + 1.00000000, 0.77987699, 0.54642268, + 1.00000000, 0.79041843, 0.56793692, + 1.00000000, 0.80053332, 0.58884417, + 1.00000000, 0.81024551, 0.60916971, + 1.00000000, 0.81957693, 0.62893653, + 1.00000000, 0.82854786, 0.64816570, + 1.00000000, 0.83717703, 0.66687674, + 1.00000000, 0.84548188, 0.68508786, + 1.00000000, 0.85347859, 0.70281616, + 1.00000000, 0.86118227, 0.72007777, + 1.00000000, 0.86860704, 0.73688797, + 1.00000000, 0.87576611, 0.75326132, + 1.00000000, 0.88267187, 0.76921169, + 1.00000000, 0.88933596, 0.78475236, + 1.00000000, 0.89576933, 0.79989606, + 1.00000000, 0.90198230, 0.81465502, + 1.00000000, 0.90963069, 0.82838210, + 1.00000000, 0.91710889, 0.84190889, + 1.00000000, 0.92441842, 0.85523742, + 1.00000000, 0.93156127, 0.86836903, + 1.00000000, 0.93853986, 0.88130458, + 1.00000000, 0.94535695, 0.89404470, + 1.00000000, 0.95201559, 0.90658983, + 1.00000000, 0.95851906, 0.91894041, + 1.00000000, 0.96487079, 0.93109690, + 1.00000000, 0.97107439, 0.94305985, + 1.00000000, 0.97713351, 0.95482993, + 1.00000000, 0.98305189, 0.96640795, + 1.00000000, 0.98883326, 0.97779486, + 1.00000000, 0.99448139, 0.98899179, + 1.00000000, 1.00000000, 1.00000000, /* 6500K */ + 0.98947904, 0.99348723, 1.00000000, + 0.97940448, 0.98722715, 1.00000000, + 0.96975025, 0.98120637, 1.00000000, + 0.96049223, 0.97541240, 1.00000000, + 0.95160805, 0.96983355, 1.00000000, + 0.94303638, 0.96443333, 1.00000000, + 0.93480451, 0.95923080, 1.00000000, + 0.92689056, 0.95421394, 1.00000000, + 0.91927697, 0.94937330, 1.00000000, + 0.91194747, 0.94470005, 1.00000000, + 0.90488690, 0.94018594, 1.00000000, + 0.89808115, 0.93582323, 1.00000000, + 0.89151710, 0.93160469, 1.00000000, + 0.88518247, 0.92752354, 1.00000000, + 0.87906581, 0.92357340, 1.00000000, + 0.87315640, 0.91974827, 1.00000000, + 0.86744421, 0.91604254, 1.00000000, + 0.86191983, 0.91245088, 1.00000000, + 0.85657444, 0.90896831, 1.00000000, + 0.85139976, 0.90559011, 1.00000000, + 0.84638799, 0.90231183, 1.00000000, + 0.84153180, 0.89912926, 1.00000000, + 0.83682430, 0.89603843, 1.00000000, + 0.83225897, 0.89303558, 1.00000000, + 0.82782969, 0.89011714, 1.00000000, + 0.82353066, 0.88727974, 1.00000000, + 0.81935641, 0.88452017, 1.00000000, + 0.81530175, 0.88183541, 1.00000000, + 0.81136180, 0.87922257, 1.00000000, + 0.80753191, 0.87667891, 1.00000000, + 0.80380769, 0.87420182, 1.00000000, + 0.80018497, 0.87178882, 1.00000000, + 0.79665980, 0.86943756, 1.00000000, + 0.79322843, 0.86714579, 1.00000000, + 0.78988728, 0.86491137, 1.00000000, /* 10000K */ + 0.78663296, 0.86273225, 1.00000000, + 0.78346225, 0.86060650, 1.00000000, + 0.78037207, 0.85853224, 1.00000000, + 0.77735950, 0.85650771, 1.00000000, + 0.77442176, 0.85453121, 1.00000000, + 0.77155617, 0.85260112, 1.00000000, + 0.76876022, 0.85071588, 1.00000000, + 0.76603147, 0.84887402, 1.00000000, + 0.76336762, 0.84707411, 1.00000000, + 0.76076645, 0.84531479, 1.00000000, + 0.75822586, 0.84359476, 1.00000000, + 0.75574383, 0.84191277, 1.00000000, + 0.75331843, 0.84026762, 1.00000000, + 0.75094780, 0.83865816, 1.00000000, + 0.74863017, 0.83708329, 1.00000000, + 0.74636386, 0.83554194, 1.00000000, + 0.74414722, 0.83403311, 1.00000000, + 0.74197871, 0.83255582, 1.00000000, + 0.73985682, 0.83110912, 1.00000000, + 0.73778012, 0.82969211, 1.00000000, + 0.73574723, 0.82830393, 1.00000000, + 0.73375683, 0.82694373, 1.00000000, + 0.73180765, 0.82561071, 1.00000000, + 0.72989845, 0.82430410, 1.00000000, + 0.72802807, 0.82302316, 1.00000000, + 0.72619537, 0.82176715, 1.00000000, + 0.72439927, 0.82053539, 1.00000000, + 0.72263872, 0.81932722, 1.00000000, + 0.72091270, 0.81814197, 1.00000000, + 0.71922025, 0.81697905, 1.00000000, + 0.71756043, 0.81583783, 1.00000000, + 0.71593234, 0.81471775, 1.00000000, + 0.71433510, 0.81361825, 1.00000000, + 0.71276788, 0.81253878, 1.00000000, + 0.71122987, 0.81147883, 1.00000000, + 0.70972029, 0.81043789, 1.00000000, + 0.70823838, 0.80941546, 1.00000000, + 0.70678342, 0.80841109, 1.00000000, + 0.70535469, 0.80742432, 1.00000000, + 0.70395153, 0.80645469, 1.00000000, + 0.70257327, 0.80550180, 1.00000000, + 0.70121928, 0.80456522, 1.00000000, + 0.69988894, 0.80364455, 1.00000000, + 0.69858167, 0.80273941, 1.00000000, + 0.69729688, 0.80184943, 1.00000000, + 0.69603402, 0.80097423, 1.00000000, + 0.69479255, 0.80011347, 1.00000000, + 0.69357196, 0.79926681, 1.00000000, + 0.69237173, 0.79843391, 1.00000000, + 0.69119138, 0.79761446, 1.00000000, /* 15000K */ + 0.69003044, 0.79680814, 1.00000000, + 0.68888844, 0.79601466, 1.00000000, + 0.68776494, 0.79523371, 1.00000000, + 0.68665951, 0.79446502, 1.00000000, + 0.68557173, 0.79370830, 1.00000000, + 0.68450119, 0.79296330, 1.00000000, + 0.68344751, 0.79222975, 1.00000000, + 0.68241029, 0.79150740, 1.00000000, + 0.68138918, 0.79079600, 1.00000000, + 0.68038380, 0.79009531, 1.00000000, + 0.67939381, 0.78940511, 1.00000000, + 0.67841888, 0.78872517, 1.00000000, + 0.67745866, 0.78805526, 1.00000000, + 0.67651284, 0.78739518, 1.00000000, + 0.67558112, 0.78674472, 1.00000000, + 0.67466317, 0.78610368, 1.00000000, + 0.67375872, 0.78547186, 1.00000000, + 0.67286748, 0.78484907, 1.00000000, + 0.67198916, 0.78423512, 1.00000000, + 0.67112350, 0.78362984, 1.00000000, + 0.67027024, 0.78303305, 1.00000000, + 0.66942911, 0.78244457, 1.00000000, + 0.66859988, 0.78186425, 1.00000000, + 0.66778228, 0.78129191, 1.00000000, + 0.66697610, 0.78072740, 1.00000000, + 0.66618110, 0.78017057, 1.00000000, + 0.66539706, 0.77962127, 1.00000000, + 0.66462376, 0.77907934, 1.00000000, + 0.66386098, 0.77854465, 1.00000000, + 0.66310852, 0.77801705, 1.00000000, + 0.66236618, 0.77749642, 1.00000000, + 0.66163375, 0.77698261, 1.00000000, + 0.66091106, 0.77647551, 1.00000000, + 0.66019791, 0.77597498, 1.00000000, + 0.65949412, 0.77548090, 1.00000000, + 0.65879952, 0.77499315, 1.00000000, + 0.65811392, 0.77451161, 1.00000000, + 0.65743716, 0.77403618, 1.00000000, + 0.65676908, 0.77356673, 1.00000000, + 0.65610952, 0.77310316, 1.00000000, + 0.65545831, 0.77264537, 1.00000000, + 0.65481530, 0.77219324, 1.00000000, + 0.65418036, 0.77174669, 1.00000000, + 0.65355332, 0.77130560, 1.00000000, + 0.65293404, 0.77086988, 1.00000000, + 0.65232240, 0.77043944, 1.00000000, + 0.65171824, 0.77001419, 1.00000000, + 0.65112144, 0.76959404, 1.00000000, + 0.65053187, 0.76917889, 1.00000000, + 0.64994941, 0.76876866, 1.00000000, /* 20000K */ + 0.64937392, 0.76836326, 1.00000000, + 0.64880528, 0.76796263, 1.00000000, + 0.64824339, 0.76756666, 1.00000000, + 0.64768812, 0.76717529, 1.00000000, + 0.64713935, 0.76678844, 1.00000000, + 0.64659699, 0.76640603, 1.00000000, + 0.64606092, 0.76602798, 1.00000000, + 0.64553103, 0.76565424, 1.00000000, + 0.64500722, 0.76528472, 1.00000000, + 0.64448939, 0.76491935, 1.00000000, + 0.64397745, 0.76455808, 1.00000000, + 0.64347129, 0.76420082, 1.00000000, + 0.64297081, 0.76384753, 1.00000000, + 0.64247594, 0.76349813, 1.00000000, + 0.64198657, 0.76315256, 1.00000000, + 0.64150261, 0.76281076, 1.00000000, + 0.64102399, 0.76247267, 1.00000000, + 0.64055061, 0.76213824, 1.00000000, + 0.64008239, 0.76180740, 1.00000000, + 0.63961926, 0.76148010, 1.00000000, + 0.63916112, 0.76115628, 1.00000000, + 0.63870790, 0.76083590, 1.00000000, + 0.63825953, 0.76051890, 1.00000000, + 0.63781592, 0.76020522, 1.00000000, + 0.63737701, 0.75989482, 1.00000000, + 0.63694273, 0.75958764, 1.00000000, + 0.63651299, 0.75928365, 1.00000000, + 0.63608774, 0.75898278, 1.00000000, + 0.63566691, 0.75868499, 1.00000000, + 0.63525042, 0.75839025, 1.00000000, + 0.63483822, 0.75809849, 1.00000000, + 0.63443023, 0.75780969, 1.00000000, + 0.63402641, 0.75752379, 1.00000000, + 0.63362667, 0.75724075, 1.00000000, + 0.63323097, 0.75696053, 1.00000000, + 0.63283925, 0.75668310, 1.00000000, + 0.63245144, 0.75640840, 1.00000000, + 0.63206749, 0.75613641, 1.00000000, + 0.63168735, 0.75586707, 1.00000000, + 0.63131096, 0.75560036, 1.00000000, + 0.63093826, 0.75533624, 1.00000000, + 0.63056920, 0.75507467, 1.00000000, + 0.63020374, 0.75481562, 1.00000000, + 0.62984181, 0.75455904, 1.00000000, + 0.62948337, 0.75430491, 1.00000000, + 0.62912838, 0.75405319, 1.00000000, + 0.62877678, 0.75380385, 1.00000000, + 0.62842852, 0.75355685, 1.00000000, + 0.62808356, 0.75331217, 1.00000000, + 0.62774186, 0.75306977, 1.00000000, /* 25000K */ + 0.62740336, 0.75282962, 1.00000000 /* 25100K */ +}; + + +static void +interpolate_color (float a, const float *c1, const float *c2, float *c) +{ + c[0] = (1.0 - a) * c1[0] + a * c2[0]; + c[1] = (1.0 - a) * c1[1] + a * c2[1]; + c[2] = (1.0 - a) * c1[2] + a * c2[2]; +} + + +/* Helper macro used in the fill functions */ +#define F(Y, C) pow ((Y) *brightness * white_point[C], 1.0 / gamma[C]) + +static void +colorramp_fill (guint16 *gamma_r, + guint16 *gamma_g, + guint16 *gamma_b, + int ramp_size, + guint32 temp) +{ + /* Approximate white point */ + float white_point[3]; + float alpha = (temp % 100) / 100.0; + int temp_index = ((temp - 1000) / 100) * 3; + float brightness = 1.0; + float gamma[3] = { 1.0, 1.0, 1.0 }; + + interpolate_color (alpha, + &blackbody_color[temp_index], + &blackbody_color[temp_index+3], + white_point); + + for (int i = 0; i < ramp_size; i++) { + gamma_r[i] = F ((double)gamma_r[i]/(G_MAXUINT16+1), 0) * (G_MAXUINT16+1); + gamma_g[i] = F ((double)gamma_g[i]/(G_MAXUINT16+1), 1) * (G_MAXUINT16+1); + gamma_b[i] = F ((double)gamma_b[i]/(G_MAXUINT16+1), 2) * (G_MAXUINT16+1); + } +} + + +void +phosh_gamma_table_fill (guint16 *table, guint32 ramp_size, guint32 temp) +{ + guint16 *r = table; + guint16 *g = table + ramp_size; + guint16 *b = table + 2 * ramp_size; + + g_return_if_fail (temp >= 1000 && temp <= 25000); + + /* Initialize gamma ramps to pure state */ + for (guint32 i = 0; i < ramp_size; i++) { + guint16 value = (double)i / ramp_size * (G_MAXUINT16+1); + r[i] = value; + g[i] = value; + b[i] = value; + } + + colorramp_fill (r, g, b, ramp_size, temp); +} diff --git a/src/monitor/gamma-table.h b/src/monitor/gamma-table.h new file mode 100644 index 000000000..45cd1c6bb --- /dev/null +++ b/src/monitor/gamma-table.h @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +void phosh_gamma_table_fill (guint16 *table, guint32 ramp_size, guint32 temp); + +G_END_DECLS diff --git a/src/monitor/head-priv.h b/src/monitor/head-priv.h new file mode 100644 index 000000000..f5131d591 --- /dev/null +++ b/src/monitor/head-priv.h @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2019-2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include "phosh-wayland.h" +#include "monitor.h" + +#include +#include +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_HEAD (phosh_head_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshHead, phosh_head, PHOSH, HEAD, GObject) + +typedef struct _PhoshHead PhoshHead; + +typedef struct _PhoshHeadMode { + /*< private >*/ + struct zwlr_output_mode_v1 *wlr_mode; + PhoshHead *head; + + int32_t width, height; + int32_t refresh; + gboolean preferred; + char *name; +} PhoshHeadMode; + +struct _PhoshHead { + GObject parent; + + char *name; + char *description; + char *vendor, *product, *serial; + gboolean enabled; + + struct PhoshPhysicalSize { + int32_t width, height; + } phys; + int32_t x, y; + + enum wl_output_transform transform; + double scale; + PhoshHeadMode *mode; + GPtrArray *modes; + + struct PhoshHeadStatePending { + int32_t x, y; + enum wl_output_transform transform; + PhoshHeadMode *mode; + double scale; + gboolean enabled; + gboolean seen; + } pending; + + PhoshMonitorConnectorType conn_type; + struct zwlr_output_head_v1 *wlr_head; +}; + + +PhoshHead *phosh_head_new_from_wlr_head (gpointer wlr_head); +struct zwlr_output_head_v1 *phosh_head_get_wlr_head (PhoshHead *self); +gboolean phosh_head_get_enabled (PhoshHead *self); +void phosh_head_set_pending_enabled (PhoshHead *self, gboolean enabled); +PhoshHeadMode *phosh_head_get_preferred_mode (PhoshHead *self); +gboolean phosh_head_is_builtin (PhoshHead *self); +PhoshHeadMode *phosh_head_find_mode_by_name (PhoshHead *self, const char *name); +void phosh_head_clear_pending (PhoshHead *self); +void phosh_head_set_pending_transform (PhoshHead *self, + PhoshMonitorTransform transform, + GPtrArray *heads); +void phosh_head_set_pending_scale (PhoshHead *self, + double scale, + GPtrArray *heads); + +G_END_DECLS diff --git a/src/monitor/head.c b/src/monitor/head.c new file mode 100644 index 000000000..38e41f7ca --- /dev/null +++ b/src/monitor/head.c @@ -0,0 +1,683 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-head" + +#include "head-priv.h" + +#include + +/** + * PhoshHead: + * + * An output head + * + * A output head (usually a monitor). Only enabled heads correspond to a + * wl_output and #PhoshMonitor. #PhoshHead should be considered an + * implementation detail of #PhoshMonitorManager and not be used outside of it. + */ + +enum { + SIGNAL_HEAD_FINISHED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +enum { + PHOSH_HEAD_PROP_0, + PHOSH_HEAD_PROP_WLR_HEAD, + PHOSH_HEAD_PROP_NAME, + PHOSH_HEAD_PROP_LAST_PROP, +}; +static GParamSpec *props[PHOSH_HEAD_PROP_LAST_PROP]; + +G_DEFINE_TYPE (PhoshHead, phosh_head, G_TYPE_OBJECT); + + +static void +phosh_head_mode_destroy (PhoshHeadMode *mode) +{ + g_return_if_fail (PHOSH_IS_HEAD (mode->head)); + + g_clear_pointer (&mode->wlr_mode, zwlr_output_mode_v1_destroy); + g_free (mode->name); + g_free (mode); +} + + +static void +mode_name (PhoshHeadMode *mode) +{ + if (mode->name) + return; + + if (!mode->refresh || !mode->width || !mode->height) + return; + + mode->name = g_strdup_printf ("%dx%d@%.0f", mode->width, mode->height, + mode->refresh / 1000.0); + +} + + +static void +zwlr_output_mode_v1_handle_size (void *data, struct zwlr_output_mode_v1 *wlr_mode, + int32_t width, int32_t height) +{ + PhoshHeadMode *mode = data; + + mode->width = width; + mode->height = height; + mode_name (mode); +} + + +static void +zwlr_output_mode_v1_handle_refresh (void *data, + struct zwlr_output_mode_v1 *wlr_mode, + int32_t refresh) +{ + PhoshHeadMode *mode = data; + + mode->refresh = refresh; + mode_name (mode); +} + + +static void +zwlr_output_mode_v1_handle_preferred (void *data, + struct zwlr_output_mode_v1 *wlr_mode) +{ + PhoshHeadMode *mode = data; + + mode->preferred = TRUE; +} + + +static void +zwlr_output_mode_v1_handle_finished (void *data, + struct zwlr_output_mode_v1 *wlr_mode) +{ + PhoshHeadMode *mode = data; + + /* Array removal triggers phosh_head_mode_destroy */ + if (!g_ptr_array_remove (mode->head->modes, mode)) + g_warning ("Failed to remove mode %p", mode); +} + + +static const struct zwlr_output_mode_v1_listener mode_listener = { + .size = zwlr_output_mode_v1_handle_size, + .refresh = zwlr_output_mode_v1_handle_refresh, + .preferred = zwlr_output_mode_v1_handle_preferred, + .finished = zwlr_output_mode_v1_handle_finished, +}; + +static PhoshHeadMode * +phosh_head_mode_new_from_wlr_mode (PhoshHead *head, struct zwlr_output_mode_v1 *wlr_mode) +{ + PhoshHeadMode *mode = g_new0 (PhoshHeadMode, 1); + + mode->wlr_mode = wlr_mode; + /* head outlives the mode so no need to take a ref */ + mode->head = head; + zwlr_output_mode_v1_add_listener (mode->wlr_mode, &mode_listener, mode); + return mode; +} + +static void +head_handle_name (void *data, + struct zwlr_output_head_v1 *head, + const char *name) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + g_debug ("Head %p is named %s", self, name); + + self->name = g_strdup (name); + + /* wlroots uses the connector's name as output name so + try to derive the connector type from it */ + self->conn_type = phosh_monitor_connector_type_from_name (name); +} + + +static void +head_handle_description (void *data, + struct zwlr_output_head_v1 *head, + const char *description) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + g_debug ("Head %p has description %s", self, description); + + self->description = g_strdup (description); +} + + +static void +head_handle_physical_size (void *data, + struct zwlr_output_head_v1 *head, + int32_t width, int32_t height) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + g_debug ("Head %p has physical size %dx%d", self, width, height); + self->phys.width = width; + self->phys.height = height; +} + + +static void +head_handle_mode (void *data, + struct zwlr_output_head_v1 *head, + struct zwlr_output_mode_v1 *wlr_mode) +{ + PhoshHead *self = PHOSH_HEAD (data); + PhoshHeadMode *mode; + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + mode = phosh_head_mode_new_from_wlr_mode (self, wlr_mode); + g_debug ("Head %p has mode %p", self, wlr_mode); + g_ptr_array_add (self->modes, mode); +} + + +static void +head_handle_enabled (void *data, + struct zwlr_output_head_v1 *head, + int32_t enabled) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + self->enabled = self->pending.enabled = !!enabled; + g_debug ("Head %p is %sabled", self, self->enabled ? "en" : "dis"); +} + + +static void +head_handle_current_mode (void *data, + struct zwlr_output_head_v1 *head, + struct zwlr_output_mode_v1 *wlr_mode) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + for (int i = 0; i < self->modes->len; i++) { + PhoshHeadMode *mode = g_ptr_array_index (self->modes, i); + + if (mode->wlr_mode == wlr_mode) { + g_debug ("Head %p has current mode %p", self, mode); + self->mode = self->pending.mode = mode; + return; + } + } + + g_warning ("Head %p received invalid current mode %px", head, wlr_mode); +} + + +static void +head_handle_position (void *data, + struct zwlr_output_head_v1 *head, + int32_t x, int32_t y) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + g_debug ("Head %p has pos %d,%d", self, x, y); + self->x = self->pending.x = x; + self->y = self->pending.y = y; +} + + +static void +head_handle_transform (void *data, + struct zwlr_output_head_v1 *head, + int32_t transform) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + g_debug ("Head %p has transform %d", self, transform); + self->transform = self->pending.transform = transform; +} + + +static void +head_handle_scale (void *data, + struct zwlr_output_head_v1 *head, + wl_fixed_t scale) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + self->scale = self->pending.scale = wl_fixed_to_double(scale); + g_debug ("Head %p has scale %f", self, self->scale); + +} + + +static void +head_handle_finished (void *data, + struct zwlr_output_head_v1 *head) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + g_debug ("Head %p finished", self); + g_signal_emit (self, signals[SIGNAL_HEAD_FINISHED], 0); +} + +static void +head_handle_make (void *data, + struct zwlr_output_head_v1 *zwlr_output_head_v1, + const char *make) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + g_free (self->vendor); + self->vendor = g_strdup (make); + g_debug ("Head %p has vendor %s", self, self->vendor); +} + + +static void +head_handle_model (void *data, + struct zwlr_output_head_v1 *zwlr_output_head_v1, + const char *model) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + g_free (self->product); + self->product = g_strdup (model); + g_debug ("Head %p has product %s", self, self->product); +} + + +static void head_handle_serial_number (void *data, + struct zwlr_output_head_v1 *zwlr_output_head_v1, + const char *serial_number) +{ + PhoshHead *self = PHOSH_HEAD (data); + + g_return_if_fail (PHOSH_IS_HEAD (self)); + + g_free (self->serial); + self->serial = g_strdup (serial_number); + g_debug ("Head %p has serial %s", self, self->serial); +} + + +static const struct zwlr_output_head_v1_listener zwlr_output_head_v1_listener = +{ + .name = head_handle_name, + .description = head_handle_description, + .physical_size = head_handle_physical_size, + .mode = head_handle_mode, + .enabled = head_handle_enabled, + .current_mode = head_handle_current_mode, + .position = head_handle_position, + .transform = head_handle_transform, + .scale = head_handle_scale, + .finished = head_handle_finished, + .make = head_handle_make, + .model = head_handle_model, + .serial_number = head_handle_serial_number, +}; + + +static void +phosh_head_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshHead *self = PHOSH_HEAD (object); + + switch (property_id) { + case PHOSH_HEAD_PROP_WLR_HEAD: + self->wlr_head = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_head_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshHead *self = PHOSH_HEAD (object); + + switch (property_id) { + case PHOSH_HEAD_PROP_WLR_HEAD: + g_value_set_pointer (value, self->wlr_head); + break; + case PHOSH_HEAD_PROP_NAME: + g_value_set_string (value, self->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_head_dispose (GObject *object) +{ + PhoshHead *self = PHOSH_HEAD (object); + + g_ptr_array_free (self->modes, TRUE); + g_clear_pointer (&self->description, g_free); + g_clear_pointer (&self->name, g_free); + g_clear_pointer (&self->vendor, g_free); + g_clear_pointer (&self->product, g_free); + g_clear_pointer (&self->serial, g_free); + g_clear_pointer (&self->wlr_head, zwlr_output_head_v1_destroy); + + G_OBJECT_CLASS (phosh_head_parent_class)->dispose (object); +} + + +static void +phosh_head_constructed (GObject *object) +{ + PhoshHead *self = PHOSH_HEAD (object); + + self->modes = g_ptr_array_new_with_free_func ((GDestroyNotify)phosh_head_mode_destroy); + zwlr_output_head_v1_add_listener (self->wlr_head, &zwlr_output_head_v1_listener, self); +} + + +static void +phosh_head_class_init (PhoshHeadClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_head_constructed; + object_class->dispose = phosh_head_dispose; + + object_class->set_property = phosh_head_set_property; + object_class->get_property = phosh_head_get_property; + + props[PHOSH_HEAD_PROP_WLR_HEAD] = + g_param_spec_pointer ("wlr-head", + "wlr-head", + "The wlr head associated with this head", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + props[PHOSH_HEAD_PROP_NAME] = + g_param_spec_string ("name", + "Name", + "The head's name", + "", + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + signals[SIGNAL_HEAD_FINISHED] = g_signal_new ("head-finished", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); + + g_object_class_install_properties (object_class, PHOSH_HEAD_PROP_LAST_PROP, props); +} + + +static void +phosh_head_init (PhoshHead *self) +{ +} + + +PhoshHead * +phosh_head_new_from_wlr_head (gpointer wlr_head) +{ + return g_object_new (PHOSH_TYPE_HEAD, "wlr-head", wlr_head, NULL); +} + + +/** + * phosh_head_set_pending_transform: + * @self: The head to transform + * @transform: The transform to apply to the head + * @heads:(element-type PhoshHead): All currently known heads + * + * Set the heads pending transform. Move pending positions of heads to + * the right and below @self around to avoid gaps and overlaps in the + * layout. + */ +void +phosh_head_set_pending_transform (PhoshHead *self, + PhoshMonitorTransform transform, + GPtrArray *heads) + +{ + int dx, dy; + gboolean was_tilted, tilted; + + g_return_if_fail (self); + was_tilted = phosh_monitor_transform_is_tilted ((PhoshMonitorTransform) self->pending.transform); + tilted = phosh_monitor_transform_is_tilted (transform); + + self->pending.transform = (enum wl_output_transform) transform; + + /* We need to adjust the positions of monitors to the right and below the transformed + ones to avoid gaps if tilting changed */ + if (was_tilted == tilted) + return; + + g_return_if_fail (self->pending.mode); + g_return_if_fail (self->pending.scale >= 0.0); + dx = (self->pending.mode->height - self->pending.mode->width) / self->pending.scale; + dy = (self->pending.mode->width - self->pending.mode->height) / self->pending.scale; + if (was_tilted) { + dx *= -1; + dy *= -1; + } + + g_debug ("Output orientation of %s changed, " + "adjusting layout: dx: %d, dy: %d", self->name, dx, dy); + + for (int i = 0; i < heads->len; i++) { + PhoshHead *move_head = g_ptr_array_index (heads, i); + + if (move_head->pending.x > self->pending.x) + move_head->pending.x += dx; + + if (move_head->pending.y > self->pending.y) + move_head->pending.y += dy; + } +} + +/** + * phosh_head_set_pending_scale: + * @self: The head to set scale for + * @scale: The scale to apply to the head + * @heads:(element-type PhoshHead): All currently known heads + * + * Set the heads pending scale. Move pending positions of heads to + * the right and below @self around to avoid gaps and overlaps in the + * layout. + */ +void +phosh_head_set_pending_scale (PhoshHead *self, double scale, GPtrArray *heads) + +{ + int dx, dy; + + g_return_if_fail (self); + + g_return_if_fail (self->pending.mode); + g_return_if_fail (self->pending.scale >= 0.0); + + dx = (self->pending.mode->height * ( 1.0 / self->pending.scale) - 1.0 / scale); + dy = (self->pending.mode->width * ( 1.0 / self->pending.scale) - 1.0 / scale); + + self->pending.scale = scale; + + g_debug ("Output orientation of %s changed, " + "adjusting layout: dx: %d, dy: %d", self->name, dx, dy); + + for (int i = 0; i < heads->len; i++) { + PhoshHead *move_head = g_ptr_array_index (heads, i); + + if (move_head->pending.x > self->pending.x) + move_head->pending.x += dx; + + if (move_head->pending.y > self->pending.y) + move_head->pending.y += dy; + } +} + +/** + * phosh_head_get_wlr_head: + * @self: The #PhoshHead + * + * Get the heads wlr_head + * + * Returns:(transfer none): The wayland head. + */ +struct zwlr_output_head_v1 * +phosh_head_get_wlr_head (PhoshHead *self) +{ + g_return_val_if_fail (PHOSH_IS_HEAD (self), NULL); + + return self->wlr_head; +} + +/** + * phosh_head_is_enabled: + * @self: The #PhoshHead + * + * Whether the head is enabled + * + * Returns: %TRUE if the head is enabled, otherwise %FALSE + */ +gboolean +phosh_head_get_enabled (PhoshHead *self) +{ + g_return_val_if_fail (PHOSH_IS_HEAD (self), TRUE); + + return self->enabled; +} + +/** + * phosh_head_get_preferred_mode: + * @self: The #PhoshHead + * + * Get the preferred mode + * + * Returns:(transfer none): The preferred mode + */ +PhoshHeadMode * +phosh_head_get_preferred_mode (PhoshHead *self) +{ + g_return_val_if_fail (PHOSH_IS_HEAD (self), NULL); + + for (int i = 0; i < self->modes->len; i++) { + PhoshHeadMode *mode = g_ptr_array_index (self->modes, i); + + if (mode->preferred) + return mode; + } + + return NULL; +} + +/** + * phosh_head_is_builtin: + * @self: A #PhoshHead + * + * Checks whether the given head is a builtin display. + * + * Returns: %TRUE if the head built in panel (e.g. laptop panel or + * phone LCD) + */ +gboolean +phosh_head_is_builtin (PhoshHead *self) +{ + g_return_val_if_fail (PHOSH_IS_HEAD (self), FALSE); + + return phosh_monitor_connector_is_builtin (self->conn_type); +} + +/** + * phosh_head_find_mode_by_name: (skip) + * @self: A #PhoshHead + * @name: The name of a mode + * + * Look up the given mode by its name. + * + * Returns: The #PhoshHeadMode if found, otherwise %NULL + */ +PhoshHeadMode * +phosh_head_find_mode_by_name (PhoshHead *self, const char *name) +{ + g_return_val_if_fail (PHOSH_IS_HEAD (self), NULL); + g_return_val_if_fail (name != NULL, NULL); + + for (int i = 0; i < self->modes->len; i++) { + PhoshHeadMode *mode = g_ptr_array_index (self->modes, i); + + if (!g_strcmp0 (mode->name, name)) + return mode; + } + return NULL; +} + +/** + * phosh_head_clear_pending: + * @self: A #PhoshHead + * + * Clear all pending state. This can be used if e.g. pending state + * was set but the output configuration not submitted. + */ +void +phosh_head_clear_pending (PhoshHead *self) +{ + self->pending.seen = FALSE; + self->pending.x = self->x; + self->pending.y = self->y; + self->pending.transform = self->transform; + self->pending.mode = self->mode; + self->pending.scale = self->scale; + self->pending.enabled = self->enabled; +} + + +/** + * phosh_head_set_pending_enabled: + * @self: A #PhoshHead + * @enabled: %TRUE if the head should be enabled, otherwise %FALSE + * + * Sets a head to pending enabled. This will become active after the next + * call to #phosh_monitor_manager_apply_monitor_config(). + */ +void +phosh_head_set_pending_enabled (PhoshHead *self, gboolean enabled) +{ + self->pending.enabled = enabled; +} diff --git a/src/monitor/meson.build b/src/monitor/meson.build new file mode 100644 index 000000000..110da4b6b --- /dev/null +++ b/src/monitor/meson.build @@ -0,0 +1,5 @@ +phosh_monitor_inc = include_directories('.') + +phosh_monitor_headers = files('gamma-table.h', 'head-priv.h', 'monitor.h') + +phosh_monitor_sources = files('gamma-table.c', 'head.c', 'monitor.c') diff --git a/src/monitor/monitor.c b/src/monitor/monitor.c new file mode 100644 index 000000000..413e70025 --- /dev/null +++ b/src/monitor/monitor.c @@ -0,0 +1,831 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#define G_LOG_DOMAIN "phosh-monitor" + +#include "gamma-table.h" +#include "monitor.h" +#include "shell-priv.h" + +#include "util.h" + +#include + +#include +#include + +#include + +/** + * PhoshMonitor: + * + * A monitor + * + * A rectangualar area in the compositor space, usually corresponds to + * physical monitor using wl_output and xdg_output Wayland protocols. + */ + +enum { + PHOSH_MONITOR_PROP_0, + PHOSH_MONITOR_PROP_WL_OUTPUT, + PHOSH_MONITOR_PROP_POWER_MODE, + PHOSH_MONITOR_PROP_N_GAMMA_ENTRIES, + PHOSH_MONITOR_PROP_LAST_PROP, +}; +static GParamSpec *props[PHOSH_MONITOR_PROP_LAST_PROP]; + +enum { + SIGNAL_CONFIGURED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +G_DEFINE_TYPE (PhoshMonitor, phosh_monitor, G_TYPE_OBJECT) + + +static void +output_handle_geometry (void *data, + struct wl_output *wl_output, + int x, + int y, + int physical_width, + int physical_height, + int subpixel, + const char *make, + const char *model, + int32_t transform) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + g_debug ("handle geometry output %p, position %d %d, size %dx%d, subpixel layout %d, " + "transform %d", + self, x, y, physical_width, physical_height, subpixel, transform); + + self->wl_output_done = FALSE; + self->x = x; + self->y = y; + self->width_mm = physical_width; + self->height_mm = physical_height; + self->subpixel = subpixel; + self->transform = transform; +} + + +static void +output_handle_done (void *data, + struct wl_output *wl_output) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + g_autoptr (GError) err = NULL; + + if (phosh_monitor_is_configured (self)) + return; + + self->wl_output_done = TRUE; + + if (!self->backlight && self->name) { + self->backlight = PHOSH_BACKLIGHT (phosh_backlight_sysfs_new (self->name, &err)); + if (!self->backlight) + g_debug ("Failed to get backlight for %s: %s", self->name, err->message); + } + + g_signal_emit (self, signals[SIGNAL_CONFIGURED], 0); +} + + +static void +output_handle_scale (void *data, + struct wl_output *wl_output, + int32_t scale) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + /* nothing to do */ + self->wl_output_done = FALSE; +} + + +static void +output_handle_mode (void *data, + struct wl_output *wl_output, + uint32_t flags, + int width, + int height, + int refresh) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + PhoshMonitorMode mode; + + g_debug ("handle mode output %p: %dx%d@%d", + self, width, height, refresh); + self->wl_output_done = FALSE; + + mode.width = width; + mode.height = height; + mode.flags = flags; + mode.refresh = refresh; + + g_array_append_val (self->modes, mode); + + if (width > self->width) + self->width = width; + + if (height > self->height) + self->height = height; + + if (flags & WL_OUTPUT_MODE_CURRENT) + self->current_mode = self->modes->len - 1; + + if (flags & WL_OUTPUT_MODE_PREFERRED) + self->preferred_mode = self->modes->len - 1; +} + + +static const struct wl_output_listener output_listener = +{ + output_handle_geometry, + output_handle_mode, + output_handle_done, + output_handle_scale, +}; + + +static void +xdg_output_v1_handle_logical_position (void *data, + struct zxdg_output_v1 *zxdg_output_v1, + int32_t x, + int32_t y) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + g_return_if_fail (PHOSH_IS_MONITOR (self)); + self->wl_output_done = FALSE; + g_debug ("Monitor %p: Logical pos: %d,%d", self, x, y); + self->logical.x = x; + self->logical.y = y; +} + + +static void +xdg_output_v1_handle_logical_size (void *data, + struct zxdg_output_v1 *zxdg_output_v1, + int32_t width, + int32_t height) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + g_return_if_fail (PHOSH_IS_MONITOR (self)); + self->wl_output_done = FALSE; + g_debug ("Monitor %p: Logical size: %dx%d", self, width, height); + self->logical.width = width; + self->logical.height = height; +} + + +static void +xdg_output_v1_handle_done (void *data, + struct zxdg_output_v1 *zxdg_output_v1) +{ + /* Nothing to be done */ +} + + +static void +xdg_output_v1_handle_name (void *data, + struct zxdg_output_v1 *zxdg_output_v1, + const char *name) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + /* wlroots uses the connector's name as xdg_output name */ + g_debug("Monitor %p: Connector name is %s", self, name); + + self->wl_output_done = FALSE; + self->name = g_strdup (name); + + /* wlroots uses the connector's name as output name so + try to derive the connector type from it */ + self->conn_type = phosh_monitor_connector_type_from_name (name); +} + + +static void +xdg_output_v1_handle_description(void *data, + struct zxdg_output_v1 *zxdg_output_v1, + const char *description) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + g_debug("Output description is %s", description); + self->description = g_strdup (description); +} + + +static const struct zxdg_output_v1_listener xdg_output_v1_listener = +{ + xdg_output_v1_handle_logical_position, + xdg_output_v1_handle_logical_size, + xdg_output_v1_handle_done, + xdg_output_v1_handle_name, + xdg_output_v1_handle_description, +}; + + +static void +wlr_output_power_handle_mode(void *data, + struct zwlr_output_power_v1 *output_power, + enum zwlr_output_power_v1_mode mode) +{ + PhoshMonitor *self = data; + PhoshMonitorPowerSaveMode m; + + g_return_if_fail (PHOSH_IS_MONITOR (self)); + + switch (mode) { + case ZWLR_OUTPUT_POWER_V1_MODE_OFF: + g_debug ("Monitor %s disabled", self->name); + m = PHOSH_MONITOR_POWER_SAVE_MODE_OFF; + break; + case ZWLR_OUTPUT_POWER_V1_MODE_ON: + g_debug ("Monitor %s enabled", self->name); + m = PHOSH_MONITOR_POWER_SAVE_MODE_ON; + break; + default: + g_return_if_reached (); + } + + self->power_mode = m; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_MONITOR_PROP_POWER_MODE]); +} + + +static void +wlr_output_power_handle_failed(void *data, + struct zwlr_output_power_v1 *output_power) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + g_return_if_fail (PHOSH_IS_MONITOR (self)); + g_warning("Failed to set output power mode for %s\n", self->name); +} + + +static const struct zwlr_output_power_v1_listener wlr_output_power_listener_v1 = { + .mode = wlr_output_power_handle_mode, + .failed = wlr_output_power_handle_failed, +}; + + +static void +handle_wl_gamma_size (void *data, struct zwlr_gamma_control_v1 *gamma_control, uint32_t size) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + self->n_gamma_entries = size; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_MONITOR_PROP_N_GAMMA_ENTRIES]); +} + + +static void handle_wl_gamma_failed (void *data, struct zwlr_gamma_control_v1 *gamma_control) +{ + PhoshMonitor *self = PHOSH_MONITOR (data); + + /* Only warn if we ever saw a non-0 gamma table so avoid warnings with e.g. headless backend */ + if (self->n_gamma_entries) + g_warning ("wl_gamma failed for %s", self->name); + g_clear_pointer (&self->gamma_control, zwlr_gamma_control_v1_destroy); +} + + +static const struct +zwlr_gamma_control_v1_listener gamma_control_listener = { + .gamma_size = handle_wl_gamma_size, + .failed = handle_wl_gamma_failed, +}; + + +static void +phosh_monitor_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshMonitor *self = PHOSH_MONITOR (object); + + switch (property_id) { + case PHOSH_MONITOR_PROP_WL_OUTPUT: + self->wl_output = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_monitor_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshMonitor *self = PHOSH_MONITOR (object); + + switch (property_id) { + case PHOSH_MONITOR_PROP_WL_OUTPUT: + g_value_set_pointer (value, self->wl_output); + break; + case PHOSH_MONITOR_PROP_POWER_MODE: + g_value_set_enum (value, self->power_mode); + break; + case PHOSH_MONITOR_PROP_N_GAMMA_ENTRIES: + g_value_set_uint (value, self->n_gamma_entries); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_monitor_dispose (GObject *object) +{ + PhoshMonitor *self = PHOSH_MONITOR (object); + + g_clear_pointer (&self->xdg_output, zxdg_output_v1_destroy); + g_clear_pointer (&self->wlr_output_power, zwlr_output_power_v1_destroy); + g_clear_pointer (&self->gamma_control, zwlr_gamma_control_v1_destroy); + + g_clear_object (&self->backlight); + + G_OBJECT_CLASS (phosh_monitor_parent_class)->dispose (object); +} + + +static void +phosh_monitor_finalize (GObject *object) +{ + PhoshMonitor *self = PHOSH_MONITOR (object); + + g_array_free (self->modes, TRUE); + self->modes = NULL; + + g_clear_pointer (&self->description, g_free); + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (phosh_monitor_parent_class)->finalize (object); +} + + +static void +phosh_monitor_constructed (GObject *object) +{ + PhoshMonitor *self = PHOSH_MONITOR (object); + struct zwlr_output_power_manager_v1 *zwlr_output_power_manager_v1; + g_autoptr (GError) err = NULL; + + wl_output_add_listener (self->wl_output, &output_listener, self); + + self->xdg_output = + zxdg_output_manager_v1_get_xdg_output(phosh_wayland_get_zxdg_output_manager_v1(phosh_wayland_get_default()), + self->wl_output); + g_return_if_fail (self->xdg_output); + zxdg_output_v1_add_listener (self->xdg_output, &xdg_output_v1_listener, self); + + zwlr_output_power_manager_v1 = phosh_wayland_get_zwlr_output_power_manager_v1 (phosh_wayland_get_default ()); + self->wlr_output_power = zwlr_output_power_manager_v1_get_output_power (zwlr_output_power_manager_v1, + self->wl_output); + g_return_if_fail (self->wlr_output_power); + zwlr_output_power_v1_add_listener(self->wlr_output_power, &wlr_output_power_listener_v1, self); + + self->gamma_control = zwlr_gamma_control_manager_v1_get_gamma_control ( + phosh_wayland_get_zwlr_gamma_control_manager_v1 (phosh_wayland_get_default ()), + self->wl_output); + g_return_if_fail (self->gamma_control); + zwlr_gamma_control_v1_add_listener (self->gamma_control, &gamma_control_listener, self); +} + + +static void +phosh_monitor_class_init (PhoshMonitorClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_monitor_constructed; + object_class->dispose = phosh_monitor_dispose; + object_class->finalize = phosh_monitor_finalize; + + object_class->set_property = phosh_monitor_set_property; + object_class->get_property = phosh_monitor_get_property; + + /** + * PhoshMonitor:wl-output: + * + * The wayland output associated with this monitor + */ + props[PHOSH_MONITOR_PROP_WL_OUTPUT] = + g_param_spec_pointer ("wl-output", "", "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + /** + * PhoshMonitor:power-mode: + * + * The power save mode for this monitor + */ + props[PHOSH_MONITOR_PROP_POWER_MODE] = + g_param_spec_enum ("power-mode", "", "", + PHOSH_TYPE_MONITOR_POWER_SAVE_MODE, + PHOSH_MONITOR_POWER_SAVE_MODE_OFF, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + /** + * PhoshMonitor:n-gamma-entries: + * + * The number of gamma entries for this monitor + */ + props[PHOSH_MONITOR_PROP_N_GAMMA_ENTRIES] = + g_param_spec_uint ("n-gamma-entries", "", "", + 0, G_MAXUINT32, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PHOSH_MONITOR_PROP_LAST_PROP, props); + + /** + * PhoshMonitor::configured: + * @monitor: The #PhoshMonitor emitting the signal. + * + * Emitted whenever a monitor is fully configured (that is it + * received all configuration data from the various wayland + * protocols). + */ + signals[SIGNAL_CONFIGURED] = g_signal_new ("configured", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + + +static void +phosh_monitor_init (PhoshMonitor *self) +{ + self->modes = g_array_new (FALSE, FALSE, sizeof(PhoshMonitorMode)); + self->power_mode = PHOSH_MONITOR_POWER_SAVE_MODE_OFF; +} + + +PhoshMonitor * +phosh_monitor_new_from_wl_output (gpointer wl_output) +{ + return g_object_new (PHOSH_TYPE_MONITOR, "wl-output", wl_output, NULL); +} + +/** + * phosh_monitor_get_current_mode: (skip) + * @self: A monitor + * + * Get the monitor's current mode + * + * Returns:(transfer none): The mode + */ +PhoshMonitorMode * +phosh_monitor_get_current_mode (PhoshMonitor *self) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR (self), NULL); + g_return_val_if_fail (self->current_mode < self->modes->len, NULL); + return &g_array_index (self->modes, PhoshMonitorMode, self->current_mode); +} + + +/** + * phosh_monitor_is_configured: + * @self: A #PhoshMonitor + * + * Returns: %TRUE if the monitor fully configured (received all + * state updates from the compositor). + */ +gboolean +phosh_monitor_is_configured (PhoshMonitor *self) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR (self), FALSE); + return self->wl_output_done; +} + + +/** + * phosh_monitor_is_builtin: + * @self: A #PhoshMonitor + * + * Returns: %TRUE if the monitor built in panel (e.g. laptop panel or + * phone LCD) + */ +gboolean +phosh_monitor_is_builtin (PhoshMonitor *self) +{ + PhoshShellDebugFlags debug_flags = phosh_shell_get_debug_flags (); + g_return_val_if_fail (PHOSH_IS_MONITOR (self), FALSE); + + if (G_UNLIKELY (debug_flags & PHOSH_SHELL_DEBUG_FLAG_FAKE_BUILTIN)) { + if (g_strcmp0 (self->name, "WL-1") == 0) + return TRUE; + else if (g_strcmp0 (self->name, "X11-1") == 0) + return TRUE; + else if (g_strcmp0 (self->name, "HEADLESS-1") == 0) + return TRUE; + } + + return phosh_monitor_connector_is_builtin (self->conn_type); +} + + +/** + * phosh_monitor_is_flipped: + * @self: A #PhoshMonitor + * + * Returns: %TRUE if the monitor's output is flipped + */ +gboolean +phosh_monitor_is_flipped (PhoshMonitor *self) +{ + switch (self->transform) { + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + case WL_OUTPUT_TRANSFORM_FLIPPED: + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return TRUE; + case WL_OUTPUT_TRANSFORM_90: + case WL_OUTPUT_TRANSFORM_270: + case WL_OUTPUT_TRANSFORM_NORMAL: + case WL_OUTPUT_TRANSFORM_180: + return FALSE; + default: + g_assert_not_reached (); + } +} + + +/** + * phosh_monitor_has_gamma: + * @self: A #PhoshMonitor + * + * Returns: %TRUE if the monitor can modify gamma + */ +gboolean +phosh_monitor_has_gamma (PhoshMonitor *self) +{ + return self->gamma_control && (self->n_gamma_entries > 0); +} + + +/** + * phosh_monitor_get_transform: + * @self: A #PhoshMonitor + * + * Returns: The monitor's output transform + */ +guint +phosh_monitor_get_transform (PhoshMonitor *self) +{ + switch (self->transform) { + case WL_OUTPUT_TRANSFORM_90: + return PHOSH_MONITOR_TRANSFORM_90; + case WL_OUTPUT_TRANSFORM_FLIPPED_90: + return PHOSH_MONITOR_TRANSFORM_FLIPPED_90; + case WL_OUTPUT_TRANSFORM_180: + return PHOSH_MONITOR_TRANSFORM_180; + case WL_OUTPUT_TRANSFORM_FLIPPED_180: + return PHOSH_MONITOR_TRANSFORM_FLIPPED_180; + case WL_OUTPUT_TRANSFORM_270: + return PHOSH_MONITOR_TRANSFORM_270; + case WL_OUTPUT_TRANSFORM_FLIPPED_270: + return PHOSH_MONITOR_TRANSFORM_FLIPPED_270; + case WL_OUTPUT_TRANSFORM_NORMAL: + return PHOSH_MONITOR_TRANSFORM_NORMAL; + case WL_OUTPUT_TRANSFORM_FLIPPED: + return PHOSH_MONITOR_TRANSFORM_FLIPPED; + default: + g_assert_not_reached (); + } +} + + +/** + * phosh_monitor_set_power_save_mode: + * @self: A #PhoshMonitor + * @mode: The #PhoshMonitorPowerSaveMode + * + * Sets monitor's power save mode. + */ +void +phosh_monitor_set_power_save_mode (PhoshMonitor *self, PhoshMonitorPowerSaveMode mode) +{ + enum zwlr_output_power_v1_mode wl_mode; + + g_return_if_fail (PHOSH_IS_MONITOR (self)); + g_return_if_fail (phosh_monitor_is_configured (self)); + g_return_if_fail (self->wlr_output_power); + + switch (mode) { + case PHOSH_MONITOR_POWER_SAVE_MODE_OFF: + wl_mode = ZWLR_OUTPUT_POWER_V1_MODE_OFF; + break; + case PHOSH_MONITOR_POWER_SAVE_MODE_ON: + wl_mode = ZWLR_OUTPUT_POWER_V1_MODE_ON; + break; + default: + g_return_if_reached (); + } + + zwlr_output_power_v1_set_mode (self->wlr_output_power, wl_mode); +} + +/** + * phosh_monitor_get_power_save_mode: + * @self: A #PhoshMonitor + * + * Returns: The current power save mode + */ +PhoshMonitorPowerSaveMode +phosh_monitor_get_power_save_mode (PhoshMonitor *self) +{ + return self->power_mode; +} + + +PhoshMonitorConnectorType +phosh_monitor_connector_type_from_name (const char *name) +{ + if (g_str_has_prefix (name, "LVDS-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_LVDS; + else if (g_str_has_prefix (name, "HDMI-A-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_HDMIA; + else if (g_str_has_prefix (name, "eDP-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_eDP; + else if (g_str_has_prefix (name, "DSI-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_DSI; + else if (g_str_has_prefix (name, "HEADLESS-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL; + else if (g_str_has_prefix (name, "WL-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL; + else if (g_str_has_prefix (name, "X11-")) + return PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL; + else + return PHOSH_MONITOR_CONNECTOR_TYPE_Unknown; +} + + +gboolean +phosh_monitor_connector_is_builtin (PhoshMonitorConnectorType conn_type) +{ + switch (conn_type) { + case PHOSH_MONITOR_CONNECTOR_TYPE_eDP: + case PHOSH_MONITOR_CONNECTOR_TYPE_LVDS: + case PHOSH_MONITOR_CONNECTOR_TYPE_DSI: + return TRUE; + case PHOSH_MONITOR_CONNECTOR_TYPE_Unknown: + case PHOSH_MONITOR_CONNECTOR_TYPE_VGA: + case PHOSH_MONITOR_CONNECTOR_TYPE_DVII: + case PHOSH_MONITOR_CONNECTOR_TYPE_DVID: + case PHOSH_MONITOR_CONNECTOR_TYPE_DVIA: + case PHOSH_MONITOR_CONNECTOR_TYPE_Composite: + case PHOSH_MONITOR_CONNECTOR_TYPE_SVIDEO: + case PHOSH_MONITOR_CONNECTOR_TYPE_Component: + case PHOSH_MONITOR_CONNECTOR_TYPE_9PinDIN: + case PHOSH_MONITOR_CONNECTOR_TYPE_DisplayPort: + case PHOSH_MONITOR_CONNECTOR_TYPE_HDMIA: + case PHOSH_MONITOR_CONNECTOR_TYPE_HDMIB: + case PHOSH_MONITOR_CONNECTOR_TYPE_TV: + case PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL: + default: + return FALSE; + } +} + +struct wl_output* +phosh_monitor_get_wl_output (PhoshMonitor *self) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR (self), NULL); + + return self->wl_output; +} + +/** + * phosh_monitor_get_fractional_scale: + * @self: The monitor + * + * Get the fractinoal scale determined from the output width and the + * current logical width. + * Returns: the fractional scale +*/ +float +phosh_monitor_get_fractional_scale (PhoshMonitor *self) +{ + float width; + PhoshMonitorMode *mode; + + g_return_val_if_fail (PHOSH_IS_MONITOR (self), 1.0); + g_return_val_if_fail (phosh_monitor_is_configured (self), 1.0); + + mode = phosh_monitor_get_current_mode (self); + g_return_val_if_fail (mode, 1.0); + + switch (self->transform) { + case PHOSH_MONITOR_TRANSFORM_NORMAL: + case PHOSH_MONITOR_TRANSFORM_180: + case PHOSH_MONITOR_TRANSFORM_FLIPPED: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_180: + width = self->logical.width; + break; + default: + width = self->logical.height; + } + return mode->width / width; +} + + +/** + * phosh_monitor_is_preferred_mode: + * @self: The monitor + * + * Checks whether the current monitor's mode is the monitor's + * preferred mode. + * + * Returns: %TRUE if the current mode is the display's preferred mode. + * Otherwise %FALSE. +*/ +gboolean +phosh_monitor_is_preferred_mode (PhoshMonitor *self) +{ + PhoshMonitorMode *mode; + + g_return_val_if_fail (PHOSH_IS_MONITOR (self), 1.0); + g_return_val_if_fail (phosh_monitor_is_configured (self), TRUE); + + mode = phosh_monitor_get_current_mode (self); + g_return_val_if_fail (mode, TRUE); + + return self->current_mode == self->preferred_mode; +} + + +/** + * phosh_monitor_transform_is_tilted: + * @transform: a #PhoshMonitorTransform + * + * We consider a transform to tilt the display if it changes the + * display orientation from portrait to landscape or vice versa. + * + * Returns: %TRUE for tilted transforms, otherwise %FALSE + */ +gboolean +phosh_monitor_transform_is_tilted (PhoshMonitorTransform transform) +{ + switch (transform) { + case PHOSH_MONITOR_TRANSFORM_NORMAL: + case PHOSH_MONITOR_TRANSFORM_180: + case PHOSH_MONITOR_TRANSFORM_FLIPPED: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_180: + return FALSE; + case PHOSH_MONITOR_TRANSFORM_90: + case PHOSH_MONITOR_TRANSFORM_270: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_90: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_270: + return TRUE; + default: + g_return_val_if_reached (FALSE); + } +} + +#include + +gboolean +phosh_monitor_set_color_temp (PhoshMonitor *self, guint32 temp) +{ + g_autofd int fd = -1; + guint16 *table; + off_t size; + + if (!phosh_monitor_has_gamma (self)) + return FALSE; + + size = self->n_gamma_entries * sizeof (guint16) * 3; + fd = phosh_create_shm_file (size); + if (fd < 0) { + g_warning ("Failed to create shm file"); + return FALSE; + } + + table = mmap (NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); + if (table == MAP_FAILED) { + g_warning ("Failed to map gamma table"); + return FALSE; + } + + phosh_gamma_table_fill (table, self->n_gamma_entries, temp); + munmap (table, size); + zwlr_gamma_control_v1_set_gamma (self->gamma_control, fd); + + return TRUE; +} diff --git a/src/monitor/monitor.h b/src/monitor/monitor.h new file mode 100644 index 000000000..30763dd8c --- /dev/null +++ b/src/monitor/monitor.h @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include "phosh-enums.h" +#include "phosh-wayland.h" +#include "backlight-sysfs.h" + +#include +#include +#include + +G_BEGIN_DECLS + +/** + * PhoshMonitorConnectorType: + * @PHOSH_MONITOR_CONNECTOR_TYPE_Unknown: unknown connector type + * @PHOSH_MONITOR_CONNECTOR_TYPE_VGA: a VGA connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_DVII: a DVII connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_DVID: a DVID connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_DVIA: a DVIA connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_Composite: a Composite connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_SVIDEO: a SVIDEO connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_LVDS: a LVDS connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_Component: a Component connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_9PinDIN: a 9PinDIN connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_DisplayPort: a DisplayPort connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_HDMIA: a HDMIA connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_HDMIB: a HDMIB connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_TV: a TV connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_eDP: a eDP connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL: a Virtual connector + * @PHOSH_MONITOR_CONNECTOR_TYPE_DSI: a DSI connector + * + * This matches the values in drm_mode.h + */ +typedef enum _PhoshMonitorConnectorType +{ + PHOSH_MONITOR_CONNECTOR_TYPE_Unknown = 0, + PHOSH_MONITOR_CONNECTOR_TYPE_VGA = 1, + PHOSH_MONITOR_CONNECTOR_TYPE_DVII = 2, + PHOSH_MONITOR_CONNECTOR_TYPE_DVID = 3, + PHOSH_MONITOR_CONNECTOR_TYPE_DVIA = 4, + PHOSH_MONITOR_CONNECTOR_TYPE_Composite = 5, + PHOSH_MONITOR_CONNECTOR_TYPE_SVIDEO = 6, + PHOSH_MONITOR_CONNECTOR_TYPE_LVDS = 7, + PHOSH_MONITOR_CONNECTOR_TYPE_Component = 8, + PHOSH_MONITOR_CONNECTOR_TYPE_9PinDIN = 9, + PHOSH_MONITOR_CONNECTOR_TYPE_DisplayPort = 10, + PHOSH_MONITOR_CONNECTOR_TYPE_HDMIA = 11, + PHOSH_MONITOR_CONNECTOR_TYPE_HDMIB = 12, + PHOSH_MONITOR_CONNECTOR_TYPE_TV = 13, + PHOSH_MONITOR_CONNECTOR_TYPE_eDP = 14, + PHOSH_MONITOR_CONNECTOR_TYPE_VIRTUAL = 15, + PHOSH_MONITOR_CONNECTOR_TYPE_DSI = 16, +} PhoshMonitorConnectorType; + +/** + * PhoshMonitorTransform: + * @PHOSH_MONITOR_TRANSFORM_NORMAL: normal + * @PHOSH_MONITOR_TRANSFORM_90: 90 degree clockwise + * @PHOSH_MONITOR_TRANSFORM_180: 180 degree clockwise + * @PHOSH_MONITOR_TRANSFORM_270: 270 degree clockwise + * @PHOSH_MONITOR_TRANSFORM_FLIPPED: flipped clockwise + * @PHOSH_MONITOR_TRANSFORM_FLIPPED_90: flipped and 90 deg + * @PHOSH_MONITOR_TRANSFORM_FLIPPED_180: flipped and 180 deg + * @PHOSH_MONITOR_TRANSFORM_FLIPPED_270: flipped and 270 deg + * + * the monitors rotation. This corresponds to the values in + * the org.gnome.Mutter.DisplayConfig DBus protocol. + */ +typedef enum _PhoshMonitorTransform +{ + PHOSH_MONITOR_TRANSFORM_NORMAL, + PHOSH_MONITOR_TRANSFORM_90, + PHOSH_MONITOR_TRANSFORM_180, + PHOSH_MONITOR_TRANSFORM_270, + PHOSH_MONITOR_TRANSFORM_FLIPPED, + PHOSH_MONITOR_TRANSFORM_FLIPPED_90, + PHOSH_MONITOR_TRANSFORM_FLIPPED_180, + PHOSH_MONITOR_TRANSFORM_FLIPPED_270, +} PhoshMonitorTransform; + + +typedef struct _PhoshMonitorMode +{ + int width, height; + int refresh; + guint32 flags; +} PhoshMonitorMode; + +/** + * PhoshMonitorPowerSaveMode: + * @PHOSH_MONITOR_POWER_SAVE_MODE_ON: The monitor is on + * @PHOSH_MONITOR_POWER_SAVE_MODE_OFF: The monitor is off (saving power) + * + * The power save mode of a monitor + */ +typedef enum _PhoshMonitorPowerSaveMode { + PHOSH_MONITOR_POWER_SAVE_MODE_OFF = 0, + PHOSH_MONITOR_POWER_SAVE_MODE_ON = 1, +} PhoshMonitorPowerSaveMode; + +#define PHOSH_TYPE_MONITOR (phosh_monitor_get_type ()) + +struct _PhoshMonitor { + GObject parent; + + struct wl_output *wl_output; + struct zxdg_output_v1 *xdg_output; + struct zwlr_output_power_v1 *wlr_output_power; + PhoshMonitorPowerSaveMode power_mode; + + int x, y, width, height; + int subpixel; + gint32 transform; + + struct PhoshLogicalSize { + gint32 x, y, width, height; + } logical; + + int width_mm; + int height_mm; + + char *description; + + GArray *modes; + guint current_mode; + guint preferred_mode; + + char *name; + PhoshMonitorConnectorType conn_type; + + gboolean wl_output_done; + + struct zwlr_gamma_control_v1 *gamma_control; + guint32 n_gamma_entries; + + PhoshBacklight *backlight; +}; + +G_DECLARE_FINAL_TYPE (PhoshMonitor, phosh_monitor, PHOSH, MONITOR, GObject) + +PhoshMonitor * phosh_monitor_new_from_wl_output (gpointer wl_output); +PhoshMonitorMode * phosh_monitor_get_current_mode (PhoshMonitor *self); +gboolean phosh_monitor_is_configured (PhoshMonitor *self); +gboolean phosh_monitor_is_builtin (PhoshMonitor *self); +gboolean phosh_monitor_is_flipped (PhoshMonitor *self); +gboolean phosh_monitor_has_gamma (PhoshMonitor *self); +gboolean phosh_monitor_set_color_temp (PhoshMonitor *self, guint32 temp); + +guint phosh_monitor_get_transform (PhoshMonitor *self); +void phosh_monitor_set_power_save_mode (PhoshMonitor *self, + PhoshMonitorPowerSaveMode mode); +PhoshMonitorPowerSaveMode phosh_monitor_get_power_save_mode (PhoshMonitor *self); +PhoshMonitorConnectorType phosh_monitor_connector_type_from_name (const char *name); +gboolean phosh_monitor_connector_is_builtin (PhoshMonitorConnectorType type); +struct wl_output * phosh_monitor_get_wl_output (PhoshMonitor *self); +float phosh_monitor_get_fractional_scale (PhoshMonitor *self); +gboolean phosh_monitor_is_preferred_mode (PhoshMonitor *self); + +gboolean phosh_monitor_transform_is_tilted (PhoshMonitorTransform transform); + + +G_END_DECLS diff --git a/src/mount-manager.c b/src/mount-manager.c new file mode 100644 index 000000000..079738071 --- /dev/null +++ b/src/mount-manager.c @@ -0,0 +1,324 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-mount-manager" + +#include "phosh-config.h" +#include "feedback-manager.h" +#include "mount-manager.h" +#include "mount-operation.h" +#include "notifications/mount-notification.h" +#include "notifications/notify-manager.h" +#include "shell-priv.h" + +#include + +#define AUTOMOUNT_KEY "automount" + +/** + * PhoshMountManager: + * + * Mount devices + * + * The #PhoshMountManager is responsible for auto mounting volumes and + * notifying about new devices. + */ + +typedef struct _PhoshMountManager { + GObject parent; + + GVolumeMonitor *monitor; + GSettings *settings; + GPtrArray *cancellables; + +} PhoshMountManager; + + +G_DEFINE_TYPE (PhoshMountManager, phosh_mount_manager, G_TYPE_OBJECT) + + +static void +on_drive_connected (PhoshMountManager *self, GDrive *drive, GVolumeMonitor *monitor) +{ + g_autofree char *name = NULL; + + g_return_if_fail (G_IS_DRIVE (drive)); + + name = g_drive_get_name (drive); + g_debug ("Drive '%s' connected", name); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + phosh_trigger_feedback ("device-added-media"); +} + + +static void +on_drive_disconnected (PhoshMountManager *self, GDrive *drive, GVolumeMonitor *monitor) +{ + g_autofree char *name = NULL; + + g_return_if_fail (G_IS_DRIVE (drive)); + + name = g_drive_get_name (drive); + g_debug ("Drive '%s' disconnected", name); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + phosh_trigger_feedback ("device-removed-media"); +} + + +static void +on_mount_finished (GVolume *vol, GAsyncResult *res, PhoshMountManager *self) +{ + g_autoptr (GError) err = NULL; + GCancellable *cancellable; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (G_IS_VOLUME (vol)); + + if (!g_volume_mount_finish (vol, res, &err)) { + g_autofree char *name = g_volume_get_name (vol); + g_warning ("Failed to mount volume '%s': %s", name, err->message); + } + + cancellable = g_object_get_data (G_OBJECT (vol), "phosh-cancel"); + g_ptr_array_remove_fast (self->cancellables, cancellable); + g_ptr_array_unref (self->cancellables); + g_object_unref (vol); + g_object_unref (self); +} + + +static void +on_volume_added (PhoshMountManager *self, GVolume *vol, GVolumeMonitor *monitor) +{ + gboolean automount; + gboolean mount_all; + GCancellable *cancellable; + g_autoptr (PhoshMountOperation) op = NULL; + + g_autoptr (GMount) mount = NULL; + g_autofree char *name = NULL; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (G_IS_VOLUME (vol)); + + name = g_volume_get_name (vol); + g_debug ("Volume added '%s'", name); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + mount_all = !!g_object_get_data (G_OBJECT (vol), "phosh-mount-all"); + /* Initial mount-all is o.k. even when locked */ + if (phosh_shell_get_locked (phosh_shell_get_default ()) && !mount_all) + return; + + mount = g_volume_get_mount (vol); + if (mount) + return; + + automount = g_settings_get_boolean (self->settings, AUTOMOUNT_KEY); + if (!automount || !g_volume_should_automount (vol)) + return; + + if (!g_volume_can_mount (vol)) { + g_debug ("Volume '%s' can not be mounted", name); + return; + } + + /* If this is not the initial 'mount-all' run allow UI interaction */ + if (!mount_all) + op = phosh_mount_operation_new (); + + cancellable = g_cancellable_new (); + g_object_set_data (G_OBJECT (vol), "phosh-cancel", cancellable); + g_ptr_array_add (self->cancellables, cancellable); + g_ptr_array_ref (self->cancellables); + g_debug ("Mounting '%s'", name); + g_volume_mount (g_object_ref (vol), G_MOUNT_MOUNT_NONE, G_MOUNT_OPERATION (op), cancellable, + (GAsyncReadyCallback)on_mount_finished, g_object_ref (self)); +} + + +static void +on_volume_removed (PhoshMountManager *self, GVolume *vol, GVolumeMonitor *monitor) +{ + g_autofree char *name = NULL; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (G_IS_VOLUME (vol)); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + name = g_volume_get_name (vol); + g_debug ("Volume '%s' removed", name); +} + + +static void +on_mount_added (PhoshMountManager *self, GMount *mount, GVolumeMonitor *monitor) +{ + g_autoptr (PhoshMountNotification) notification = NULL; + PhoshNotifyManager *nm; + guint id; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (G_IS_MOUNT (mount)); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + /* No notifications when mounting all volumes e.g. on startup */ + if (phosh_shell_get_locked (phosh_shell_get_default ())) + return; + + nm = phosh_notify_manager_get_default (); + id = phosh_notify_manager_get_notification_id (nm); + notification = phosh_mount_notification_new_from_mount (id, mount); + + g_object_set_data (G_OBJECT (mount), "phosh-notify-id", GINT_TO_POINTER (id)); + phosh_notify_manager_add_notification (nm, PHOSH_APP_ID ".desktop", -1, + PHOSH_NOTIFICATION (notification)); +} + + +static void +on_mount_removed (PhoshMountManager *self, GMount *mount, GVolumeMonitor *monitor) +{ + g_autofree char *name = NULL; + PhoshNotifyManager *nm; + gpointer data; + int id; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (G_IS_MOUNT (mount)); + + if (!phosh_shell_is_session_active (phosh_shell_get_default ())) + return; + + data = g_object_get_data (G_OBJECT (mount), "phosh-notify-id"); + if (!data) + return; + + id = GPOINTER_TO_INT (data); + name = g_mount_get_name (mount); + g_debug ("Mount '%s' removed, id %d", name, id); + nm = phosh_notify_manager_get_default (); + phosh_notify_manager_close_notification_by_id (nm, id, PHOSH_NOTIFICATION_REASON_UNDEFINED); +} + + +static void +on_session_active_changed (PhoshMountManager *self, GParamSpec *pspec, PhoshSessionManager *sm) +{ + /* on_volume_added takes a ref on vol itself so we can cleanup here */ + g_autolist (GVolume) volumes = NULL; + gboolean active; + + g_return_if_fail (PHOSH_IS_MOUNT_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (sm)); + + active = phosh_shell_is_session_active (phosh_shell_get_default ()); + g_debug ("Session active: %d", active); + + if (!active) + return; + + /* oneshot */ + g_signal_handlers_disconnect_by_func (sm, on_session_active_changed, self); + + g_object_connect (self->monitor, + "swapped-signal::drive-connected", on_drive_connected, self, + "swapped-signal::drive-disconnected", on_drive_disconnected, self, + "swapped-signal::volume-added", on_volume_added, self, + "swapped-signal::volume-removed", on_volume_removed, self, + "swapped-signal::mount-added", on_mount_added, self, + "swapped-signal::mount-removed", on_mount_removed, self, + NULL); + volumes = g_volume_monitor_get_volumes (self->monitor); + for (GList *elem = volumes; elem != NULL; elem = elem->next) { + GVolume *vol = G_VOLUME (elem->data); + /* Ignore screen lock on initial startup */ + g_object_set_data (G_OBJECT (vol), "phosh-mount-all", GINT_TO_POINTER (TRUE)); + on_volume_added (self, vol, self->monitor); + } + + return; +} + + +static void +phosh_mount_manager_constructed (GObject *object) +{ + PhoshMountManager *self = PHOSH_MOUNT_MANAGER (object); + PhoshSessionManager *sm; + + self->cancellables = g_ptr_array_new_with_free_func (g_object_unref); + self->monitor = g_volume_monitor_get (); + self->settings = g_settings_new ("org.gnome.desktop.media-handling"); + + sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + /* Wait for session to become active initially to mount everything so we don't + race against #PhoshSessionManager binding to the bus */ + g_signal_connect_object (sm, + "notify::active", + G_CALLBACK (on_session_active_changed), + self, + G_CONNECT_SWAPPED); + on_session_active_changed (self, NULL, sm); + + G_OBJECT_CLASS (phosh_mount_manager_parent_class)->constructed (object); +} + + +static void +phosh_mount_manager_dispose (GObject *object) +{ + PhoshMountManager *self = PHOSH_MOUNT_MANAGER (object); + + g_clear_object (&self->settings); + g_clear_object (&self->monitor); + + /* Cancel all ongoing mount operations */ + for (int i = 0; i < self->cancellables->len; i++) { + GCancellable *cancellable = g_ptr_array_index (self->cancellables, i); + g_cancellable_cancel (cancellable); + } + g_clear_pointer (&self->cancellables, g_ptr_array_unref); + + G_OBJECT_CLASS (phosh_mount_manager_parent_class)->dispose (object); +} + + +static void +phosh_mount_manager_class_init (PhoshMountManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_mount_manager_constructed; + object_class->dispose = phosh_mount_manager_dispose; +} + + +static void +phosh_mount_manager_init (PhoshMountManager *self) +{ +} + + +PhoshMountManager * +phosh_mount_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_MOUNT_MANAGER, NULL); +} diff --git a/src/mount-manager.h b/src/mount-manager.h new file mode 100644 index 000000000..f14c0c567 --- /dev/null +++ b/src/mount-manager.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MOUNT_MANAGER phosh_mount_manager_get_type () + +G_DECLARE_FINAL_TYPE (PhoshMountManager, phosh_mount_manager, + PHOSH, MOUNT_MANAGER, GObject) + +PhoshMountManager *phosh_mount_manager_new (void); + +G_END_DECLS diff --git a/src/mount-operation.c b/src/mount-operation.c new file mode 100644 index 000000000..41f32566b --- /dev/null +++ b/src/mount-operation.c @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-mount-operation" + +#include "phosh-config.h" + +#include "gtk-mount-prompt.h" +#include "mount-operation.h" +#include "util.h" + +/** + * PhoshMountOperation: + * + * #GMountOperation using UI + * + * A #GMountOperation that uses system modal dialogs for input. + */ + +struct _PhoshMountOperation { + GMountOperation parent; + + PhoshGtkMountPrompt *prompt; +}; +G_DEFINE_TYPE (PhoshMountOperation, phosh_mount_operation, G_TYPE_MOUNT_OPERATION) + + +static void +on_prompt_done (PhoshMountOperation *self, PhoshGtkMountPrompt *prompt) +{ + gboolean cancelled; + GMountOperationResult result = G_MOUNT_OPERATION_ABORTED; + + g_return_if_fail (PHOSH_IS_MOUNT_OPERATION (self)); + g_return_if_fail (PHOSH_IS_GTK_MOUNT_PROMPT (prompt)); + + cancelled = phosh_gtk_mount_prompt_get_cancelled (prompt); + g_debug ("Prompt done, cancelled: %d", cancelled); + + if (!cancelled) { + GAskPasswordFlags flags = phosh_gtk_mount_prompt_get_ask_flags (prompt); + + if (flags & G_ASK_PASSWORD_NEED_PASSWORD) { + const char *password; + + password = phosh_gtk_mount_prompt_get_password (prompt); + g_mount_operation_set_password (G_MOUNT_OPERATION (self), password); + result = G_MOUNT_OPERATION_HANDLED; + } + } + + g_mount_operation_reply (G_MOUNT_OPERATION (self), result); + g_clear_pointer (&self->prompt, phosh_cp_widget_destroy); +} + + +static void +new_prompt (PhoshMountOperation *self, + const char *message, + const char *icon_name, + const char *default_user, + const char *default_domain, + GVariant *pids, + const char *const *choices, + GAskPasswordFlags ask_flags) +{ + g_debug ("New prompt for '%s'", message); + + g_clear_pointer (&self->prompt, phosh_cp_widget_destroy); + + self->prompt = PHOSH_GTK_MOUNT_PROMPT (phosh_gtk_mount_prompt_new ( + message, + icon_name, + default_user, + default_domain, + pids, + choices, + ask_flags)); + g_signal_connect_swapped (self->prompt, + "closed", + G_CALLBACK (on_prompt_done), + self); + + gtk_widget_set_visible (GTK_WIDGET (self->prompt), TRUE); +} + + +static void +phosh_mount_operation_ask_password (GMountOperation *op, + const char *message, + const char *default_user, + const char *default_domain, + GAskPasswordFlags flags) +{ + PhoshMountOperation *self = PHOSH_MOUNT_OPERATION (op); + + new_prompt (self, message, + NULL, /* icon */ + default_user, + default_domain, + NULL, /* default_user */ + NULL, /* choices */ + flags); +} + + +static void +phosh_mount_operation_dispose (GObject *object) +{ + PhoshMountOperation *self = PHOSH_MOUNT_OPERATION (object); + + g_clear_pointer (&self->prompt, phosh_cp_widget_destroy); + + G_OBJECT_CLASS (phosh_mount_operation_parent_class)->dispose (object); +} + + +static void +phosh_mount_operation_class_init (PhoshMountOperationClass *klass) +{ + GMountOperationClass *mount_op_class = G_MOUNT_OPERATION_CLASS (klass); + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_mount_operation_dispose; + + mount_op_class->ask_password = phosh_mount_operation_ask_password; +} + + +static void +phosh_mount_operation_init (PhoshMountOperation *self) +{ +} + + +PhoshMountOperation * +phosh_mount_operation_new (void) +{ + return PHOSH_MOUNT_OPERATION (g_object_new (PHOSH_TYPE_MOUNT_OPERATION, NULL)); +} diff --git a/src/mount-operation.h b/src/mount-operation.h new file mode 100644 index 000000000..fa10454ee --- /dev/null +++ b/src/mount-operation.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MOUNT_OPERATION (phosh_mount_operation_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshMountOperation, phosh_mount_operation, PHOSH, MOUNT_OPERATION, GMountOperation) + +PhoshMountOperation *phosh_mount_operation_new (void); + +G_END_DECLS diff --git a/src/mpris-manager.c b/src/mpris-manager.c new file mode 100644 index 000000000..7d4fd846a --- /dev/null +++ b/src/mpris-manager.c @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-mpris-manager" + +#include "phosh-config.h" + +#include "mpris-dbus.h" +#include "mpris-manager.h" +#include "util.h" + +#include + +#define MPRIS_OBJECT_PATH "/org/mpris/MediaPlayer2" +#define MPRIS_PREFIX "org.mpris.MediaPlayer2." + +/** + * PhoshMprisManager: + * + * The #PhoshMprisManger interfaces with + * [org.mpris.MediaPlayer2](https://specifications.freedesktop.org/mpris-spec/latest/) + * based players allowing widgets to get an actual media player object. + */ + +enum { + PROP_0, + PROP_CAN_RAISE, + PROP_PLAYER, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshMprisManager { + GObject parent; + + GCancellable *cancel; + /* Base interface to raise player */ + PhoshDBusMediaPlayer2 *mpris; + /* Actual player controls */ + PhoshDBusMediaPlayer2Player *player; + GDBusConnection *session_bus; + + guint dbus_id; + + gboolean can_raise; + + GListStore *known_players; +}; +G_DEFINE_TYPE (PhoshMprisManager, phosh_mpris_manager, G_TYPE_OBJECT) + + +static void +phosh_mpris_manager_set_player (PhoshMprisManager *self, PhoshDBusMediaPlayer2Player *player) +{ + if (self->player == player) + return; + + g_set_object (&self->player, player); + g_debug ("New player: %p", self->player); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PLAYER]); +} + + +static void +phosh_mpris_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (object); + + switch (property_id) { + case PROP_CAN_RAISE: + g_value_set_boolean (value, self->can_raise); + break; + case PROP_PLAYER: + g_value_set_object (value, self->player); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_mpris_manager_dispose (GObject *object) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (self->dbus_id) { + g_dbus_connection_signal_unsubscribe (self->session_bus, self->dbus_id); + self->dbus_id = 0; + } + + g_clear_object (&self->session_bus); + g_clear_object (&self->mpris); + g_clear_object (&self->known_players); + g_clear_object (&self->player); + + G_OBJECT_CLASS (phosh_mpris_manager_parent_class)->dispose (object); +} + + +static void +phosh_mpris_manager_class_init (PhoshMprisManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_mpris_manager_get_property; + object_class->dispose = phosh_mpris_manager_dispose; + + /** + * PhoshMprisManager:can-raise + * + * Whether the player app can be raised + */ + props[PROP_CAN_RAISE] = + g_param_spec_boolean ("can-raise", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshMprisManager:player + * + * The current player. + */ + props[PROP_PLAYER] = + g_param_spec_object ("player", "", "", + PHOSH_DBUS_TYPE_MEDIA_PLAYER2_PLAYER_PROXY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static gboolean +cmp_by_name (gconstpointer a, gconstpointer b) +{ + GDBusProxy *proxy1 = G_DBUS_PROXY (a); + GDBusProxy *proxy2 = G_DBUS_PROXY (b); + const char *name1, *name2; + + name1 = g_dbus_proxy_get_name (proxy1); + name2 = g_dbus_proxy_get_name (proxy2); + + return g_strcmp0 (name1, name2) == 0; +} + + +static void +add_to_known_players (PhoshMprisManager *self, PhoshDBusMediaPlayer2Player *player) +{ + if (g_list_store_find_with_equal_func (self->known_players, + player, + cmp_by_name, + NULL)) { + return; + } + + g_debug ("Player %s not yet known, adding to known players", + g_dbus_proxy_get_name (G_DBUS_PROXY (player))); + g_list_store_append (self->known_players, player); +} + + +static void +remove_from_known_players (PhoshMprisManager *self, const char *name) +{ + for (int i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->known_players)); i++) { + g_autoptr (GDBusProxy) proxy = NULL; + + proxy = G_DBUS_PROXY (g_list_model_get_item (G_LIST_MODEL (self->known_players), i)); + + if (g_strcmp0 (g_dbus_proxy_get_name (proxy), name) == 0) { + g_debug ("Removing '%s' from known players", name); + g_list_store_remove (self->known_players, i); + return; + } + } +} + + +static void +on_attach_player_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (user_data); + g_autoptr (PhoshDBusMediaPlayer2Player) player = NULL; + g_autoptr (GError) err = NULL; + + player = phosh_dbus_media_player2_player_proxy_new_for_bus_finish (res, &err); + if (player == NULL) { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + phosh_async_error_warn (err, "Failed to get player"); + phosh_mpris_manager_set_player (self, NULL); + return; + } + + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + + add_to_known_players (self, player); + phosh_mpris_manager_set_player (self, player); +} + + +static void +on_attach_mpris_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (user_data); + PhoshDBusMediaPlayer2 *mpris; + g_autoptr (GError) err = NULL; + gboolean can_raise; + + mpris = phosh_dbus_media_player2_proxy_new_for_bus_finish (res, &err); + /* Missing mpris interface is not fatal */ + if (mpris == NULL) { + phosh_async_error_warn (err, "Failed to get player"); + return; + } + + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + self->mpris = g_steal_pointer (&mpris); + + can_raise = phosh_dbus_media_player2_get_can_raise (self->mpris); + if (self->can_raise == can_raise) + return; + + self->can_raise = can_raise; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_RAISE]); +} + + +static void +attach_player (PhoshMprisManager *self, const char *name) +{ + g_clear_object (&self->mpris); + phosh_mpris_manager_set_player (self, NULL); + + g_debug ("Trying to attach player for %s", name); + + /* The player interface with the controls */ + phosh_dbus_media_player2_player_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + name, + MPRIS_OBJECT_PATH, + self->cancel, + on_attach_player_ready, + self); + + /* The player base interface to e.g. raise the player */ + phosh_dbus_media_player2_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + name, + MPRIS_OBJECT_PATH, + self->cancel, + on_attach_mpris_ready, + self); +} + + +static gboolean +is_valid_player (const char *bus_name) +{ + if (!g_str_has_prefix (bus_name, MPRIS_PREFIX)) + return FALSE; + + if (strlen (bus_name) < G_N_ELEMENTS (MPRIS_PREFIX)) + return FALSE; + + return TRUE; +} + + +static void +find_player_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (user_data); + g_autoptr (GVariant) result = NULL; + g_autoptr (GVariant) names = NULL; + g_autoptr (GError) err = NULL; + const char *name; + GVariantIter iter; + gboolean found = FALSE; + + result = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), res, &err); + if (!result) { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + phosh_async_error_warn (err, "Failed to list bus names to find mpris player"); + + phosh_mpris_manager_set_player (self, NULL); + return; + } + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_CONNECTION (self->session_bus)); + + names = g_variant_get_child_value (result, 0); + g_variant_iter_init (&iter, names); + while (g_variant_iter_loop (&iter, "&s", &name)) { + + if (!is_valid_player (name)) + continue; + + g_debug ("Found player: %s", name); + attach_player (self, name); + found = TRUE; + break; + } + + if (!found) { + g_debug ("No player found"); + phosh_mpris_manager_set_player (self, NULL); + } +} + + +static void +find_player (PhoshMprisManager *self) +{ + g_return_if_fail (G_IS_DBUS_CONNECTION (self->session_bus)); + + g_dbus_connection_call (self->session_bus, + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "ListNames", + NULL, + G_VARIANT_TYPE ("(as)"), + G_DBUS_CALL_FLAGS_NO_AUTO_START, + 1000, + self->cancel, + find_player_done, + self); +} + + +static void +on_dbus_name_owner_changed (GDBusConnection *connection, + const char *sender_name, + const char *object_path, + const char *interface_name, + const char *signal_name, + GVariant *parameters, + gpointer user_data) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (user_data); + g_autofree char *name, *from, *to; + + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + + g_variant_get (parameters, "(sss)", &name, &from, &to); + if (!is_valid_player (name)) + return; + + g_debug ("mpris player name owner change: '%s' '%s' '%s'", name, from, to); + + /* Current player vanished, look for another one, already running */ + if (gm_str_is_null_or_empty (to)) { + phosh_mpris_manager_set_player (self, NULL); + remove_from_known_players (self, name); + find_player (self); + return; + } + + /* New player showed up, pick it up */ + attach_player (self, name); +} + + +static void +on_bus_get_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshMprisManager *self = PHOSH_MPRIS_MANAGER (user_data); + g_autoptr (GError) err = NULL; + GDBusConnection *session_bus; + + session_bus = g_bus_get_finish (res, &err); + if (session_bus == NULL) { + phosh_async_error_warn (err, "Failed to attach to session bus"); + return; + } + + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + self->session_bus = session_bus; + /* Listen for name owner changes to detect new mpris players */ + /* We don't need to hold a ref since the callback won't be invoked after unsubscribe + * from the same thread */ + self->dbus_id = g_dbus_connection_signal_subscribe (self->session_bus, + "org.freedesktop.DBus", + "org.freedesktop.DBus", + "NameOwnerChanged", + "/org/freedesktop/DBus", + NULL, + G_DBUS_SIGNAL_FLAGS_NONE, + on_dbus_name_owner_changed, + self, NULL); + /* Find player initially */ + find_player (self); +} + + +static void +phosh_mpris_manager_init (PhoshMprisManager *self) +{ + self->cancel = g_cancellable_new (); + self->known_players = g_list_store_new (PHOSH_DBUS_TYPE_MEDIA_PLAYER2_PLAYER); + + g_bus_get (G_BUS_TYPE_SESSION, + self->cancel, + on_bus_get_finished, + self); +} + + +PhoshMprisManager * +phosh_mpris_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_MPRIS_MANAGER, NULL); +} + + +PhoshDBusMediaPlayer2Player * +phosh_mpris_manager_get_player (PhoshMprisManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MPRIS_MANAGER (self), NULL); + + return self->player; +} + + +gboolean +phosh_mpris_manager_get_can_raise (PhoshMprisManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MPRIS_MANAGER (self), FALSE); + + return self->can_raise; +} + + +gboolean +phosh_mpris_manager_raise_finish (PhoshMprisManager *self, GAsyncResult *res, GError **err) +{ + g_assert (G_IS_TASK (res)); + g_assert (!err || !*err); + g_assert (g_async_result_is_tagged (res, phosh_mpris_manager_raise_async)); + + g_return_val_if_fail (PHOSH_IS_MPRIS_MANAGER (self), FALSE); + + return g_task_propagate_boolean (G_TASK (res), err); +} + + +static void +on_raise_done (GObject *object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (GTask) task = G_TASK (user_data); + PhoshDBusMediaPlayer2 *mpris = PHOSH_DBUS_MEDIA_PLAYER2 (object); + GError *err = NULL; + + if (!phosh_dbus_media_player2_call_raise_finish (mpris, res, &err)) { + g_task_return_error (task, err); + return; + } + + g_task_return_boolean (task, TRUE); +} + + +void +phosh_mpris_manager_raise_async (PhoshMprisManager *self, + GCancellable *cancel, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = NULL; + + g_return_if_fail (PHOSH_IS_MPRIS_MANAGER (self)); + g_return_if_fail (cancel == NULL || G_IS_CANCELLABLE (cancel)); + + task = g_task_new (self, cancel, callback, user_data); + g_task_set_source_tag (task, phosh_mpris_manager_raise_async); + + phosh_dbus_media_player2_call_raise (self->mpris, + self->cancel, + on_raise_done, + g_steal_pointer (&task)); +} + + +GListModel * +phosh_mpris_manager_get_known_players (PhoshMprisManager *self) +{ + g_return_val_if_fail (PHOSH_IS_MPRIS_MANAGER (self), NULL); + + return G_LIST_MODEL (self->known_players); +} diff --git a/src/mpris-manager.h b/src/mpris-manager.h new file mode 100644 index 000000000..42d79ea48 --- /dev/null +++ b/src/mpris-manager.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "mpris-dbus.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MPRIS_MANAGER (phosh_mpris_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshMprisManager, phosh_mpris_manager, PHOSH, MPRIS_MANAGER, GObject) + +PhoshMprisManager * phosh_mpris_manager_new (void); +PhoshDBusMediaPlayer2Player * + phosh_mpris_manager_get_player (PhoshMprisManager *self); +gboolean phosh_mpris_manager_get_can_raise (PhoshMprisManager *self); +void phosh_mpris_manager_raise_async (PhoshMprisManager *self, + GCancellable *cancel, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean phosh_mpris_manager_raise_finish (PhoshMprisManager *self, + GAsyncResult *res, + GError **err); + +GListModel * phosh_mpris_manager_get_known_players (PhoshMprisManager *self); + +G_END_DECLS diff --git a/src/network-auth-manager.c b/src/network-auth-manager.c new file mode 100644 index 000000000..6b568e260 --- /dev/null +++ b/src/network-auth-manager.c @@ -0,0 +1,763 @@ +/* + * Copyright (C) 2019-2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#define G_LOG_DOMAIN "phosh-network-auth-manager" + +#include "phosh-config.h" + +#include "contrib/shell-network-agent.h" +#include "network-auth-manager.h" +#include "network-auth-prompt.h" +#include "shell-priv.h" +#include "phosh-wayland.h" +#include "util.h" + +#include + +#include +#include +#include + +/** + * PhoshNetworkAuthManager: + * + * Handles the interaction between networkmanager and the auth prompts + * + * Wi-Fi and other credentials are handled with #ShellNetworkAgent which implements + * #NMSecretAgentOld to interface with NetworkManager. When a credential for a connection are requested, + * a new #PhoshNetworkAuthPrompt is created, which asks the user various + * credentials depending on the connection type and details (e.g. access point security method). + * + * For VPN prompts the plugins auth helper is being run and the the list of secrets to request + * is fed to #PhoshNetworkAuthPrompt. + * + * TODO: Support more connection types + */ + +struct _PhoshNetworkAuthManager { + GObject parent; + + GCancellable *cancel; + GCancellable *register_cancel; + + NMDeviceWifi *dev; + ShellNetworkAgent *network_agent; + PhoshNetworkAuthPrompt *network_prompt; +}; +G_DEFINE_TYPE (PhoshNetworkAuthManager, phosh_network_auth_manager, G_TYPE_OBJECT); + + +static void +network_prompt_done_cb (PhoshNetworkAuthManager *self) +{ + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + + g_clear_pointer ((PhoshSystemModalDialog**)&self->network_prompt, + phosh_system_modal_dialog_close); +} + + +static void +network_agent_setup_prompt (PhoshNetworkAuthManager *self) +{ + GtkWidget *network_prompt; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + + if (self->network_prompt) + return; + + network_prompt = phosh_network_auth_prompt_new (self->network_agent); + self->network_prompt = PHOSH_NETWORK_AUTH_PROMPT (network_prompt); + + g_signal_connect_object (self->network_prompt, "done", + G_CALLBACK (network_prompt_done_cb), + self, G_CONNECT_SWAPPED); + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + self->network_prompt, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); +} + + +static PhoshNMSecret * +phosh_nm_secret_new (const char *name, const char* key, const char *value, gboolean is_pw) +{ + PhoshNMSecret *secret = g_new0 (PhoshNMSecret, 1); + + g_debug ("New VPN secret '%s/%s/%s/%d", name, key, value, is_pw); + + secret->name = g_strdup (name); + secret->key = g_strdup (key); + secret->value = g_strdup (value); + secret->is_pw = is_pw; + + return secret; +} + +static void +phosh_nm_secret_free (PhoshNMSecret *secret) +{ + g_free (secret->name); + g_free (secret->key); + g_free (secret->value); +} + + +typedef struct vpn_request { + PhoshNetworkAuthManager *self; + char *request_id; + NMConnection *connection; + char *setting_name; + char **hints; + NMSecretAgentGetSecretsFlags flags; + GCancellable *cancel; +} VPNRequest; + + +static void +vpn_request_free (VPNRequest *request) +{ + g_object_unref (request->self); + g_free (request->request_id); + g_object_unref (request->connection); + g_free (request->setting_name); + g_strfreev (request->hints); + g_object_unref (request->cancel); +} + +static void +vpn_request_error (VPNRequest *request, GError *error) +{ + g_warning ("Failed to get VPN secrets: %s", error->message); + shell_network_agent_respond (request->self->network_agent, request->request_id, + SHELL_NETWORK_AGENT_INTERNAL_ERROR); +} + +typedef struct { + GPid auth_helper_pid; + GString *auth_helper_response; + VPNRequest *request; + GPtrArray *secrets; + GCancellable *cancellable; + gulong cancellable_id; + guint child_watch_id; + GInputStream *input_stream; + GOutputStream *output_stream; + char read_buf[5]; +} AuthHelperData; + + +static void +auth_helper_data_free (AuthHelperData *data) +{ + g_clear_signal_handler (&data->cancellable_id, data->cancellable); + g_clear_object (&data->cancellable); + g_clear_handle_id (&data->child_watch_id, g_source_remove); + g_spawn_close_pid (data->auth_helper_pid); + g_string_free (data->auth_helper_response, TRUE); + g_clear_object (&data->input_stream); + g_clear_object (&data->output_stream); + g_free (data); +} + + +static gboolean +str_to_bool (const char *str) +{ + g_autofree char *tmp = NULL; + + if (!str) + return FALSE; + + tmp = g_ascii_strdown (str, -1); + if (g_strcmp0 (tmp, "true") == 0 || + g_strcmp0 (tmp, "yes") == 0 || + g_strcmp0 (tmp, "1") == 0 || + g_strcmp0 (tmp, "on") == 0) { + return TRUE; + } + return FALSE; +} + + +/* @phosh_clear_cancellable_disconnect: + * @cancellable: a #GCancellable + * @cancellable_id: An id retrieved when connecting to the @cancellable's `cancelled` signal + * + * If @cancellable_id is not 0, clear it and call g_cancellable_disconnect(). + * @cancellable may be %NULL, if there is nothing to disconnect. + */ +static inline gboolean +phosh_clear_g_cancellable_disconnect (GCancellable *cancellable, gulong *cancellable_id) +{ + gulong id; + + g_return_val_if_fail (G_IS_CANCELLABLE (cancellable), FALSE); + + id = *cancellable_id; + if (cancellable_id && id != 0) { + *cancellable_id = 0; + g_cancellable_disconnect (cancellable, id); + return TRUE; + } + return FALSE; +} + +#define VPN_UI_GROUP "VPN Plugin UI" + +static void +auth_helper_exited (GPid pid, int status, gpointer user_data) +{ + AuthHelperData * data = user_data; + VPNRequest * request = data->request; + NMSettingVpn * s_vpn = nm_connection_get_setting_vpn (request->connection); + + g_autoptr (GKeyFile) keyfile = NULL; + g_autofree char * title = NULL; + g_autofree char * message = NULL; + g_auto (GStrv) groups = NULL; + + g_autoptr (GError) error = NULL; + g_autoptr (GPtrArray) secrets = NULL; + + data->child_watch_id = 0; + g_debug ("Auth helper exited with: %d", status); + + phosh_clear_g_cancellable_disconnect (data->cancellable, &data->cancellable_id); + + if (status != 0) { + g_set_error (&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Auth dialog failed with error code %d", + status); + goto out; + } + + keyfile = g_key_file_new (); + if (!g_key_file_load_from_data (keyfile, + data->auth_helper_response->str, + data->auth_helper_response->len, + G_KEY_FILE_NONE, + &error)) { + goto out; + } + + groups = g_key_file_get_groups (keyfile, NULL); + if (g_strcmp0 (groups[0], VPN_UI_GROUP)) { + g_set_error (&error, + NM_SECRET_AGENT_ERROR, + NM_SECRET_AGENT_ERROR_FAILED, + "Expected [VPN Plugin UI] in auth dialog response"); + goto out; + } + + title = g_key_file_get_string (keyfile, "VPN Plugin UI", "Title", &error); + if (!title) + goto out; + + message = g_key_file_get_string (keyfile, "VPN Plugin UI", "Description", &error); + if (!message) + goto out; + + secrets = g_ptr_array_new_with_free_func ((GDestroyNotify) phosh_nm_secret_free); + for (int i = 1; groups[i]; i++) { + g_autofree char *pretty_name = NULL; + g_autofree char *value = NULL; + PhoshNMSecret *secret; + + if (g_strcmp0 (groups[i], VPN_UI_GROUP) == 0) + continue; + + value = g_key_file_get_string (keyfile, groups[i], "Value", NULL); + if (!g_key_file_get_boolean (keyfile, groups[i], "IsSecret", NULL)) + continue; + if (g_key_file_get_boolean (keyfile, groups[i], "ShouldAsk", NULL)) { + pretty_name = g_key_file_get_string (keyfile, groups[i], "Label", NULL); + secret = phosh_nm_secret_new (pretty_name, + groups[i], + nm_setting_vpn_get_service_type (s_vpn), + g_key_file_get_boolean (keyfile, groups[i], "IsSecret", NULL)); + g_ptr_array_add (secrets, secret); + } else { + if (gm_str_is_null_or_empty (value)) + continue; + + shell_network_agent_add_vpn_secret (request->self->network_agent, + request->request_id, + groups[i], + value); + } + } + +out: + if (error) { + vpn_request_error (request, error); + } else { + if (secrets && secrets->len) { + gboolean success; + network_agent_setup_prompt (request->self); + success = phosh_network_auth_prompt_set_request (request->self->network_prompt, + request->request_id, + request->connection, + request->setting_name, + request->hints, + request->flags, + title, + message, + secrets); + if (!success) { + /* TODO: queue request and process once prompt is done */ + g_warning ("Dropping request %s since prompt already busy", request->request_id); + shell_network_agent_respond (request->self->network_agent, + request->request_id, + SHELL_NETWORK_AGENT_USER_CANCELED); + } + } else { + g_debug ("Skipping VPN dialog for %s", request->request_id); + shell_network_agent_respond (request->self->network_agent, + request->request_id, + SHELL_NETWORK_AGENT_CONFIRMED); + } + } + auth_helper_data_free (data); +} + +static void +_request_cancelled (GObject *object, gpointer user_data) +{ + auth_helper_data_free (user_data); +} + +static void +auth_helper_read_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream * auth_helper_out = G_INPUT_STREAM (source_object); + AuthHelperData *data = user_data; + gssize read_size; + + g_autoptr (GError) error = NULL; + + read_size = g_input_stream_read_finish (auth_helper_out, res, &error); + switch (read_size) { + case -1: + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + vpn_request_error (data->request, error); + auth_helper_data_free (data); + break; + case 0: + /* Done reading. Let's wait for the auth dialog to exit so that we're able to collect the status. + * Remember we can be cancelled in between. */ + data->child_watch_id = g_child_watch_add (data->auth_helper_pid, auth_helper_exited, data); + data->cancellable = g_object_ref (data->request->cancel); + data->cancellable_id = g_cancellable_connect (data->cancellable, + G_CALLBACK (_request_cancelled), data, NULL); + break; + default: + g_string_append_len (data->auth_helper_response, data->read_buf, read_size); + g_input_stream_read_async (auth_helper_out, + data->read_buf, + sizeof(data->read_buf), + G_PRIORITY_DEFAULT, + NULL, + auth_helper_read_done, + data); + return; + } + + g_input_stream_close (auth_helper_out, NULL, NULL); +} + +static void +auth_helper_write_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GOutputStream *auth_helper_out = G_OUTPUT_STREAM (source_object); + char *auth_helper_request_str = user_data; + + g_free (auth_helper_request_str); + /* We don't care about write errors. If there are any problems, the + * reader shall notice. */ + g_output_stream_write_finish (auth_helper_out, res, NULL); + g_output_stream_close (auth_helper_out, NULL, NULL); +} + +static void +add_to_string (GString *string, const char *key, const char *value) +{ + g_auto (GStrv) lines = NULL; + int i; + + lines = g_strsplit (value, "\n", -1); + + g_string_append (string, key); + for (i = 0; lines[i]; i++) { + g_string_append_c (string, '='); + g_string_append (string, lines[i]); + g_string_append_c (string, '\n'); + } +} + +static void +add_data_item_to_string (const char *key, const char *value, gpointer user_data) +{ + GString *string = user_data; + + add_to_string (string, "DATA_KEY", key); + add_to_string (string, "DATA_VAL", value); + g_string_append_c (string, '\n'); +} + +static void +add_secret_to_string (const char *key, const char *value, gpointer user_data) +{ + GString *string = user_data; + + add_to_string (string, "SECRET_KEY", key); + add_to_string (string, "SECRET_VAL", value); + g_string_append_c (string, '\n'); +} + + +static void +on_search_vpn_plugin_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + VPNRequest *request = user_data; + NMSettingVpn *vpn_setting; + int auth_helper_stdin_fd, auth_helper_stdout_fd; + GPid auth_helper_pid; + const char *str; + GOutputStream *auth_helper_stdin; + GInputStream *auth_helper_stdout; + + g_autoptr (GPtrArray) auth_helper_argv = NULL; + g_autoptr (NMVpnPluginInfo) plugin = NULL; + g_autoptr (GError) err = NULL; + const char *service_type; + + GString * auth_helper_request; + g_autofree char * auth_helper_request_str = NULL; + gsize auth_helper_request_len; + AuthHelperData * data; + + vpn_setting = nm_connection_get_setting_vpn (request->connection); + service_type = nm_setting_vpn_get_service_type (vpn_setting); + + plugin = shell_network_agent_search_vpn_plugin_finish (request->self->network_agent, + res, + &err); + if (!plugin) { + g_warning ("Failed to lookup VPN plugin for %s: %s", service_type, err->message); + goto err; + } + + str = nm_vpn_plugin_info_lookup_property (plugin, "GNOME", "supports-external-ui-mode"); + if (!str_to_bool (str)) { + g_warning ("VPN auth plugin for %s does not support external ui mode", service_type); + goto err; + } + + str = nm_vpn_plugin_info_get_auth_dialog (plugin); + if (!g_file_test (str, G_FILE_TEST_IS_EXECUTABLE)) { + g_warning ("VPN auto plugin for %s (%s) not executable", service_type, str); + goto err; + } + + auth_helper_argv = g_ptr_array_new (); + g_ptr_array_add (auth_helper_argv, (gpointer) str); + g_ptr_array_add (auth_helper_argv, "-u"); + g_ptr_array_add (auth_helper_argv, (gpointer) nm_connection_get_uuid (request->connection)); + g_ptr_array_add (auth_helper_argv, "-n"); + g_ptr_array_add (auth_helper_argv, (gpointer) nm_connection_get_id (request->connection)); + g_ptr_array_add (auth_helper_argv, "-s"); + g_ptr_array_add (auth_helper_argv, (gpointer) nm_setting_vpn_get_service_type (vpn_setting)); + g_ptr_array_add (auth_helper_argv, "--external-ui-mode"); + if (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_ALLOW_INTERACTION) + g_ptr_array_add (auth_helper_argv, "-i"); + if (request->flags & NM_SECRET_AGENT_GET_SECRETS_FLAG_REQUEST_NEW) + g_ptr_array_add (auth_helper_argv, "-r"); + + str = nm_vpn_plugin_info_lookup_property (plugin, "GNOME", "supports-hints"); + if (str_to_bool (str)) { + for (int i = 0; request->hints[i]; i++) { + g_ptr_array_add (auth_helper_argv, "-t"); + g_ptr_array_add (auth_helper_argv, request->hints[i]); + } + } + + g_ptr_array_add (auth_helper_argv, NULL); + + if (!g_spawn_async_with_pipes (NULL, + (char **) auth_helper_argv->pdata, + NULL, /* envp */ + G_SPAWN_DO_NOT_REAP_CHILD, + NULL, /* setup-func */ + NULL, /* user_data */ + &auth_helper_pid, + &auth_helper_stdin_fd, + &auth_helper_stdout_fd, + NULL, + &err)) { + const char *helper = ((char **)auth_helper_argv->pdata)[0]; + g_warning ("Failed to spawn auth helper %s: %s", helper, err->message); + goto err; + } + + auth_helper_stdin = g_unix_output_stream_new (auth_helper_stdin_fd, TRUE); + auth_helper_stdout = g_unix_input_stream_new (auth_helper_stdout_fd, TRUE); + + auth_helper_request = g_string_new_len (NULL, 1024); + nm_setting_vpn_foreach_data_item (vpn_setting, add_data_item_to_string, auth_helper_request); + nm_setting_vpn_foreach_secret (vpn_setting, add_secret_to_string, auth_helper_request); + g_string_append (auth_helper_request, "DONE\nQUIT\n"); + auth_helper_request_len = auth_helper_request->len; + auth_helper_request_str = g_string_free (auth_helper_request, FALSE); + + data = g_new0 (AuthHelperData, 1); + *data = (AuthHelperData){ + .auth_helper_response = g_string_new_len (NULL, sizeof(data->read_buf)), + .auth_helper_pid = auth_helper_pid, + .request = request, + .secrets = NULL, // g_ptr_array_ref (secrets), + .input_stream = auth_helper_stdout, + .output_stream = auth_helper_stdin, + }; + + g_output_stream_write_async (auth_helper_stdin, + auth_helper_request_str, + auth_helper_request_len, + G_PRIORITY_DEFAULT, + request->cancel, + auth_helper_write_done, + auth_helper_request_str); + + /* Ownership of the pointer was passed on to g_output_stream_write_async(). */ + g_steal_pointer (&auth_helper_request_str); + + g_input_stream_read_async (auth_helper_stdout, + data->read_buf, + sizeof(data->read_buf), + G_PRIORITY_DEFAULT, + request->cancel, + auth_helper_read_done, + data); + return; +err: + shell_network_agent_respond (request->self->network_agent, request->request_id, + SHELL_NETWORK_AGENT_INTERNAL_ERROR); + vpn_request_free (request); +} + + +static void +vpn_secret_request (PhoshNetworkAuthManager *self, + char *request_id, + NMConnection *connection, + char *setting_name, + char **hints, + NMSecretAgentGetSecretsFlags flags) +{ + NMSettingVpn *setting = nm_connection_get_setting_vpn (connection); + const char *service_type = nm_setting_vpn_get_service_type (setting); + VPNRequest *request = g_new0 (VPNRequest, 1); + + g_debug ("Handling VPN secrets for %s, flags: 0x%x", service_type, flags); + + request->self = g_object_ref (self); + request->request_id = g_strdup (request_id); + request->connection = g_object_ref (connection); + request->setting_name = g_strdup (setting_name); + request->hints = g_strdupv (hints); + request->flags = flags; + request->cancel = g_cancellable_new (); + + shell_network_agent_search_vpn_plugin (self->network_agent, service_type, + on_search_vpn_plugin_ready, + request); +} + + +static void +secret_request_new_cb (PhoshNetworkAuthManager *self, + char *request_id, + NMConnection *connection, + char *setting_name, + char **hints, + NMSecretAgentGetSecretsFlags flags, + ShellNetworkAgent *agent) +{ + gboolean ret; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + + g_debug ("Request %s: wants secrets for %s connection", request_id, + nm_connection_get_connection_type (connection)); + + if (nm_connection_is_type (connection, NM_SETTING_VPN_SETTING_NAME)) { + vpn_secret_request (self, request_id, connection, setting_name, hints, flags); + return; + } + if (!nm_connection_is_type (connection, NM_SETTING_WIRELESS_SETTING_NAME)) { + g_warning ("%s secret handling currently not supported", nm_connection_get_connection_type (connection)); + shell_network_agent_respond (self->network_agent, request_id, SHELL_NETWORK_AGENT_USER_CANCELED); + return; + } + + g_return_if_fail (!self->network_prompt); + + network_agent_setup_prompt (self); + ret = phosh_network_auth_prompt_set_request (self->network_prompt, + request_id, connection, setting_name, + hints, flags, NULL, NULL, NULL); + if (!ret) { + /* TODO: queue request and process once prompt is done */ + g_warning ("Dropping request %s since prompt already busy", request_id); + shell_network_agent_respond (self->network_agent, request_id, SHELL_NETWORK_AGENT_USER_CANCELED); + } +} + + +static void +secret_request_cancelled_cb (PhoshNetworkAuthManager *self, + char *request_id, + ShellNetworkAgent *agent) +{ + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + g_return_if_fail (SHELL_IS_NETWORK_AGENT (agent)); + + network_prompt_done_cb (self); +} + + +static void +secret_agent_register_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + PhoshNetworkAuthManager *self; + NMSecretAgentOld *agent = NM_SECRET_AGENT_OLD (object); + + g_autoptr (GError) error = NULL; + + if (!nm_secret_agent_old_register_finish (agent, result, &error)) { + g_message ("Error registering network agent: %s", error->message); + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + self = PHOSH_NETWORK_AUTH_MANAGER (user_data); + g_clear_object (&self->register_cancel); + } + return; + } + + self = PHOSH_NETWORK_AUTH_MANAGER (user_data); + g_clear_object (&self->register_cancel); + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + + g_signal_connect_object (self->network_agent, "new-request", + G_CALLBACK (secret_request_new_cb), + self, G_CONNECT_SWAPPED); + g_signal_connect_object (self->network_agent, "cancel-request", + G_CALLBACK (secret_request_cancelled_cb), + self, G_CONNECT_SWAPPED); +} + + +static void +on_network_agent_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) err = NULL; + PhoshNetworkAuthManager *self; + GObject *nw_agent; + + nw_agent = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object), res, &err); + if (!nw_agent) { + phosh_async_error_warn (err, "Failed to init network agent"); + return; + } + + self = PHOSH_NETWORK_AUTH_MANAGER (user_data); + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + self->network_agent = SHELL_NETWORK_AGENT (nw_agent); + self->register_cancel = g_cancellable_new (); + nm_secret_agent_old_register_async (NM_SECRET_AGENT_OLD (self->network_agent), self->register_cancel, + secret_agent_register_cb, self); +} + + +static void +setup_network_agent (PhoshNetworkAuthManager *self) +{ + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_MANAGER (self)); + + g_async_initable_new_async (SHELL_TYPE_NETWORK_AGENT, + G_PRIORITY_DEFAULT, + self->cancel, + on_network_agent_ready, + self, + "capabilities", NM_SECRET_AGENT_CAPABILITY_VPN_HINTS, + "identifier", PHOSH_APP_ID ".NetworkAgent", + "auto-register", FALSE, + NULL); +} + + +static void +phosh_network_auth_manager_constructed (GObject *object) +{ + PhoshNetworkAuthManager *self = PHOSH_NETWORK_AUTH_MANAGER (object); + + self->cancel = g_cancellable_new (); + + setup_network_agent (self); + g_debug ("Network-auth-manager initialized"); + + G_OBJECT_CLASS (phosh_network_auth_manager_parent_class)->constructed (object); +} + + +static void +phosh_network_auth_manager_dispose (GObject *object) +{ + PhoshNetworkAuthManager *self = PHOSH_NETWORK_AUTH_MANAGER (object); + + g_cancellable_cancel (self->register_cancel); + g_clear_object (&self->register_cancel); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->network_agent); + + G_OBJECT_CLASS (phosh_network_auth_manager_parent_class)->dispose (object); +} + + +static void +phosh_network_auth_manager_class_init (PhoshNetworkAuthManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_network_auth_manager_constructed; + object_class->dispose = phosh_network_auth_manager_dispose; +} + + +static void +phosh_network_auth_manager_init (PhoshNetworkAuthManager *self) +{ +} + + +PhoshNetworkAuthManager * +phosh_network_auth_manager_new (void) +{ + return PHOSH_NETWORK_AUTH_MANAGER (g_object_new (PHOSH_TYPE_NETWORK_AUTH_MANAGER, NULL)); +} diff --git a/src/network-auth-manager.h b/src/network-auth-manager.h new file mode 100644 index 000000000..a15a38b5b --- /dev/null +++ b/src/network-auth-manager.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_NETWORK_AUTH_MANAGER (phosh_network_auth_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshNetworkAuthManager, phosh_network_auth_manager, PHOSH, NETWORK_AUTH_MANAGER, GObject) + +PhoshNetworkAuthManager *phosh_network_auth_manager_new (void); + +G_END_DECLS diff --git a/src/network-auth-prompt.c b/src/network-auth-prompt.c new file mode 100644 index 000000000..de5b4a8c4 --- /dev/null +++ b/src/network-auth-prompt.c @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Mohammed Sadiq + */ + +#define G_LOG_DOMAIN "phosh-network-auth-prompt" + +#include "phosh-config.h" + +#include "contrib/shell-network-agent.h" +#include "network-auth-prompt.h" +#include "password-entry.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include + +#include + +/** + * PhoshNetworkAuthPrompt: + * + * A modal prompt for asking Network credentials + * + * The #PhoshNetworkAuthPrompt is used to request network credentials + * The responses are then passed to NetworkManager's #ShellNetworkAgent. + */ + +enum { + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +struct _PhoshNetworkAuthPrompt +{ + PhoshSystemModalDialog parent; + + GtkWidget *cancel_button; + GtkWidget *connect_button; + GtkWidget *message_label; + GtkWidget *main_box; + + GtkWidget *wpa_grid; + GtkWidget *wpa_password_entry; + GcrSecureEntryBuffer *password_buffer; + + GtkWidget *vpn_grid; + + NMConnection *connection; + const char *key_type; + char *request_id; + char *setting_name; + NMUtilsSecurityType security_type; + GPtrArray *secrets; + NMSecretAgentGetSecretsFlags flags; + + ShellNetworkAgent *agent; + + gboolean visible; /* is input visible */ +}; + +G_DEFINE_TYPE(PhoshNetworkAuthPrompt, phosh_network_auth_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + + +static void +emit_done (PhoshNetworkAuthPrompt *self, gboolean cancelled) +{ + g_debug ("Emitting done. Cancelled: %d", cancelled); + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + if (!self->request_id) + return; + + g_clear_pointer (&self->request_id, g_free); + g_signal_emit (self, signals[DONE], 0 /* detail */, cancelled); +} + + +static gboolean +security_has_proto (NMSettingWirelessSecurity *sec, const char *item) +{ + g_return_val_if_fail (sec, FALSE); + g_return_val_if_fail (item, FALSE); + + for (guint32 i = 0; i < nm_setting_wireless_security_get_num_protos (sec); i++) { + if (strcmp (item, nm_setting_wireless_security_get_proto (sec, i)) == 0) + return TRUE; + } + + return FALSE; +} + + +static NMUtilsSecurityType +network_prompt_get_type (PhoshNetworkAuthPrompt *self) +{ + NMSettingWirelessSecurity *setting; + const char *key_mgmt, *auth_alg; + + g_return_val_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self), NMU_SEC_NONE); + g_return_val_if_fail (self->connection, NMU_SEC_NONE); + + setting = nm_connection_get_setting_wireless_security (self->connection); + + if (!setting) + return NMU_SEC_NONE; + + key_mgmt = nm_setting_wireless_security_get_key_mgmt (setting); + auth_alg = nm_setting_wireless_security_get_auth_alg (setting); + + if (strcmp (key_mgmt, "none") == 0) + return NMU_SEC_STATIC_WEP; + + if (strcmp (key_mgmt, "ieee8021x") == 0) { + if (auth_alg && strcmp (auth_alg, "leap") == 0) + return NMU_SEC_LEAP; + return NMU_SEC_DYNAMIC_WEP; + } + + if (strcmp (key_mgmt, "sae") == 0 || + strcmp (key_mgmt, "wpa-none") == 0 || + strcmp (key_mgmt, "wpa-psk") == 0) { + if (security_has_proto (setting, "rsn")) + return NMU_SEC_WPA2_PSK; + else + return NMU_SEC_WPA_PSK; + } + + if (strcmp (key_mgmt, "wpa-eap") == 0) { + if (security_has_proto (setting, "rsn")) + return NMU_SEC_WPA2_ENTERPRISE; + else + return NMU_SEC_WPA_ENTERPRISE; + } + + return NMU_SEC_INVALID; +} + + +static const char * +network_connection_get_key_type (NMConnection *connection) +{ + NMSettingWirelessSecurity *setting; + const char *key_mgmt; + + g_return_val_if_fail (NM_IS_CONNECTION (connection), NULL); + + setting = nm_connection_get_setting_wireless_security (connection); + key_mgmt = nm_setting_wireless_security_get_key_mgmt (setting); + + g_return_val_if_fail (key_mgmt, "psk"); + + if (g_str_equal (key_mgmt, "none")) + return "wep-key0"; + + /* Assume WPA/WPA2 Personal */ + return "psk"; +} + + +static void +network_prompt_set_grid (PhoshNetworkAuthPrompt *self, GtkWidget *grid) +{ + g_autoptr (GList) children = gtk_container_get_children (GTK_CONTAINER (self->main_box)); + + if (children) + gtk_container_remove (GTK_CONTAINER (self->main_box), GTK_WIDGET (children->data)); + + gtk_container_add (GTK_CONTAINER (self->main_box), grid); +} + + +static void +network_prompt_setup_wifi_dialog (PhoshNetworkAuthPrompt *self) +{ + NMSettingWireless *setting; + g_autofree char *str = NULL; + g_autofree char *ssid = NULL; + GBytes *bytes; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + setting = nm_connection_get_setting_wireless (self->connection); + self->key_type = network_connection_get_key_type (self->connection); + self->security_type = network_prompt_get_type (self); + + /* TODO: Do this in network-auth-manager and set message */ + bytes = nm_setting_wireless_get_ssid (setting); + ssid = nm_utils_ssid_to_utf8 (g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes)); + + if (self->security_type != NMU_SEC_WPA_PSK && + self->security_type != NMU_SEC_WPA2_PSK && + self->security_type != NMU_SEC_STATIC_WEP) { + g_debug ("Network security method %d of %s not supported", + self->security_type, ssid); + str = g_strdup_printf(_( + "Authentication type of Wi-Fi network “%s” not supported"), ssid); + gtk_label_set_label (GTK_LABEL (self->message_label), str); + return; + } + + str = g_strdup_printf (_("Enter password for the Wi-Fi network “%s”"), ssid); + gtk_label_set_label (GTK_LABEL (self->message_label), str); + + network_prompt_set_grid (self, self->wpa_grid); + + /* Load password */ + if (self->security_type != NMU_SEC_NONE) { + NMSettingWirelessSecurity *wireless_setting; + const char *password = ""; + + wireless_setting = nm_connection_get_setting_wireless_security (self->connection); + + if (self->security_type == NMU_SEC_WPA_PSK || + self->security_type == NMU_SEC_WPA2_PSK) + password = nm_setting_wireless_security_get_psk (wireless_setting); + else if (self->security_type == NMU_SEC_STATIC_WEP) { + int index; + + index = nm_setting_wireless_security_get_wep_tx_keyidx (wireless_setting); + password = nm_setting_wireless_security_get_wep_key (wireless_setting, index); + } + + if (!password) + password = ""; + + gtk_entry_buffer_set_text (GTK_ENTRY_BUFFER (self->password_buffer), password, -1); + } + + gtk_widget_grab_focus (self->wpa_password_entry); +} + + +static void +on_network_prompt_password_changed (PhoshNetworkAuthPrompt *self, GtkEntry *entry) +{ + const char *password; + PhoshNMSecret *secret; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + g_return_if_fail (GTK_IS_ENTRY (entry)); + + password = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (self->password_buffer)); + + secret = g_object_get_data (G_OBJECT (entry), "secret"); + g_return_if_fail (secret); + g_free (secret->value); + secret->value = g_strdup (gtk_entry_get_text (entry)); + + if (!password || !*password) + return + + gtk_widget_set_sensitive (self->connect_button, TRUE); +} + + +static GtkWidget * +build_credentials_entry (PhoshNetworkAuthPrompt *self, PhoshNMSecret *secret) +{ + GtkEntryBuffer *buffer = gcr_secure_entry_buffer_new (); + GtkWidget *entry = g_object_new (PHOSH_TYPE_PASSWORD_ENTRY, + "valign", GTK_ALIGN_CENTER, + "hexpand", TRUE, + "activates-default", TRUE, + "buffer", buffer, + NULL); + g_object_set_data (G_OBJECT (entry), "secret", secret); + g_signal_connect_swapped (entry, + "changed", + G_CALLBACK (on_network_prompt_password_changed), + self); + + return entry; +} + + +static void +network_prompt_setup_vpn_dialog (PhoshNetworkAuthPrompt *self) +{ + g_autoptr (GList) children = NULL; + gboolean focus = FALSE; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + network_prompt_set_grid (self, self->vpn_grid); + + children = gtk_container_get_children (GTK_CONTAINER (self->vpn_grid)); + for (GList *elem = children; elem; elem = elem->next) + gtk_container_remove (GTK_CONTAINER (self->vpn_grid), GTK_WIDGET (elem->data)); + + for (int i = 0; i < self->secrets->len; i++) { + g_autofree char *l = NULL; + PhoshNMSecret *secret = g_ptr_array_index (self->secrets, i); + GtkWidget *label = NULL; + GtkWidget *entry = build_credentials_entry (self, secret); + + if (g_str_has_suffix (secret->name, ":")) + l = g_strdup (secret->name); + else + l = g_strdup_printf ("%s:", secret->name); + + label = g_object_new (GTK_TYPE_LABEL, "label", l, "halign", GTK_ALIGN_END, NULL); + + gtk_widget_set_visible (label, TRUE); + gtk_widget_set_visible (entry, TRUE); + + gtk_grid_attach (GTK_GRID (self->vpn_grid), label, 1, i, 1, 1); + gtk_grid_attach (GTK_GRID (self->vpn_grid), entry, 2, i, 1, 1); + + if (!focus) { + gtk_widget_grab_focus (entry); + focus = TRUE; + } + } +} + + +static void +network_prompt_setup_dialog (PhoshNetworkAuthPrompt *self, const char *title, const char *message) +{ + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + if (nm_connection_is_type (self->connection, NM_SETTING_WIRELESS_SETTING_NAME)) { + network_prompt_setup_wifi_dialog (self); + } else if (nm_connection_is_type (self->connection, NM_SETTING_VPN_SETTING_NAME)) { + network_prompt_setup_vpn_dialog (self); + } else { + g_assert_not_reached (); + } + + if (title) + phosh_system_modal_dialog_set_title (PHOSH_SYSTEM_MODAL_DIALOG (self), title); + + if (message) + gtk_label_set_label (GTK_LABEL (self->message_label), message); +} + + +static void +on_dialog_canceled (PhoshNetworkAuthPrompt *self) +{ + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + shell_network_agent_respond (self->agent, self->request_id, SHELL_NETWORK_AGENT_USER_CANCELED); + emit_done (self, TRUE); +} + + +static void +network_prompt_connect_clicked_cb (PhoshNetworkAuthPrompt *self) +{ + const char *password; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + if (g_strcmp0 (self->setting_name, NM_SETTING_VPN_SETTING_NAME) == 0) { + for (int i = 0; i < self->secrets->len; i++) { + PhoshNMSecret *secret = g_ptr_array_index (self->secrets, i); + + shell_network_agent_add_vpn_secret (self->agent, self->request_id, secret->key, secret->value); + } + g_clear_pointer (&self->secrets, g_ptr_array_unref); + shell_network_agent_respond (self->agent, self->request_id, SHELL_NETWORK_AGENT_CONFIRMED); + } else { + password = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (self->password_buffer)); + shell_network_agent_set_password (self->agent, self->request_id, + (char *) self->key_type, (char *) password); + shell_network_agent_respond (self->agent, self->request_id, SHELL_NETWORK_AGENT_CONFIRMED); + } + + emit_done (self, FALSE); +} + + +static void +phosh_network_auth_prompt_finalize (GObject *object) +{ + PhoshNetworkAuthPrompt *self = PHOSH_NETWORK_AUTH_PROMPT (object); + + g_free (self->request_id); + g_free (self->setting_name); + g_clear_pointer (&self->secrets, g_ptr_array_unref); + + g_clear_object (&self->agent); + g_clear_object (&self->connection); + + G_OBJECT_CLASS (phosh_network_auth_prompt_parent_class)->finalize (object); +} + + +static void +network_prompt_wpa_password_changed_cb (PhoshNetworkAuthPrompt *self) +{ + const char *password; + gboolean valid = FALSE; + + g_return_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self)); + + password = gtk_entry_buffer_get_text (GTK_ENTRY_BUFFER (self->password_buffer)); + + if (!password || !*password) { + /* do nothing */ + } else if (self->security_type == NMU_SEC_WPA_PSK || + self->security_type == NMU_SEC_WPA2_PSK) { + valid = nm_utils_wpa_psk_valid (password); + } else if (self->security_type == NMU_SEC_STATIC_WEP) { + valid = nm_utils_wep_key_valid (password, NM_WEP_KEY_TYPE_PASSPHRASE); + valid |= nm_utils_wep_key_valid (password, NM_WEP_KEY_TYPE_KEY); + } + + gtk_widget_set_sensitive (self->connect_button, valid); +} + + +static void +phosh_network_auth_prompt_class_init (PhoshNetworkAuthPromptClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_network_auth_prompt_finalize; + + /** + * PhoshNetworkAuthPrompt::done: + * @self: The network auth prompt + * @cancelled: whether the prompt was cancelled + * + * This signal is emitted when the prompt can be closed. The cancelled + * argument indicates whether the prompt was cancelled. + */ + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_BOOLEAN); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/network-auth-prompt.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, cancel_button); + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, connect_button); + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, message_label); + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, main_box); + + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, wpa_grid); + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, wpa_password_entry); + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, password_buffer); + + gtk_widget_class_bind_template_child (widget_class, PhoshNetworkAuthPrompt, vpn_grid); + + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); + gtk_widget_class_bind_template_callback (widget_class, network_prompt_connect_clicked_cb); + gtk_widget_class_bind_template_callback (widget_class, network_prompt_wpa_password_changed_cb); +} + + +static void +phosh_network_auth_prompt_init (PhoshNetworkAuthPrompt *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_network_auth_prompt_new (ShellNetworkAgent *agent) +{ + PhoshNetworkAuthPrompt *self; + + g_return_val_if_fail (SHELL_IS_NETWORK_AGENT (agent), NULL); + + self = g_object_new (PHOSH_TYPE_NETWORK_AUTH_PROMPT, NULL); + self->agent = g_object_ref (agent); + + return GTK_WIDGET (self); +} + + +/** + * phosh_network_auth_prompt_set_request: + * @self: The prompt + * @request_id: The unique id of this authentication request + * @connection: The network manager connection + * @setting_name: The connection setting name (e.g. 'vpn') + * @hints: auth request hints (currently unused) + * @flags: Secret flags + * @title: The prompt title + * @message: The prompt message + * @secrets: (nullable)(element-type PhoshNMSecret): The secrets to get + * + * Sets up a network authentication prompt for an auth request. + * Returns: %TRUE if success otherwise (e.g. if the prompt is still in use) %FALSE + */ +gboolean +phosh_network_auth_prompt_set_request (PhoshNetworkAuthPrompt *self, + char *request_id, + NMConnection *connection, + char *setting_name, + char **hints, + NMSecretAgentGetSecretsFlags flags, + const char *title, + const char *message, + GPtrArray *secrets) +{ + g_return_val_if_fail (PHOSH_IS_NETWORK_AUTH_PROMPT (self), FALSE); + g_return_val_if_fail (NM_IS_CONNECTION (connection), FALSE); + + /* We only handle one request at a time */ + if (self->request_id) { + g_debug ("Trying to reuse prompt with request %s for new request %s", self->request_id, request_id); + return FALSE; + } + + g_free (self->setting_name); + self->request_id = g_strdup (request_id); + self->setting_name = g_strdup (setting_name); + if (secrets) + self->secrets = g_ptr_array_ref (secrets); + g_set_object (&self->connection, connection); + self->flags = flags; + + network_prompt_setup_dialog (self, title, message); + + return TRUE; +} diff --git a/src/network-auth-prompt.h b/src/network-auth-prompt.h new file mode 100644 index 000000000..e61f1cd6b --- /dev/null +++ b/src/network-auth-prompt.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include +#include +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +/** + * PhoshNMSecret: + * @name: The secrets name + * @key: The key that identifies it to the nm plugin + * @value: The value (secret) + * @is_pw: Whether this is a secret + * + * Used for secret transfer between #PhoshNetworkAuthManager and #PhoshNetworkAuthPrompt + */ +typedef struct phosh_nm_secret { + char *name; + char *key; + char *value; + gboolean is_pw; +} PhoshNMSecret; + +#define PHOSH_TYPE_NETWORK_AUTH_PROMPT (phosh_network_auth_prompt_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshNetworkAuthPrompt, phosh_network_auth_prompt, PHOSH, NETWORK_AUTH_PROMPT, PhoshSystemModalDialog); + +GtkWidget *phosh_network_auth_prompt_new (ShellNetworkAgent *agent); +gboolean phosh_network_auth_prompt_set_request (PhoshNetworkAuthPrompt *self, + char *request_id, + NMConnection *connection, + char *setting_name, + char **hints, + NMSecretAgentGetSecretsFlags flags, + const char *title, + const char *message, + GPtrArray *secrets); +G_END_DECLS diff --git a/src/notifications/dbus-notification.c b/src/notifications/dbus-notification.c new file mode 100644 index 000000000..a23d1ed22 --- /dev/null +++ b/src/notifications/dbus-notification.c @@ -0,0 +1,145 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-dbus-notification" + +#include "phosh-config.h" + +#include "dbus-notification.h" +#include "notify-manager.h" +#include "shell-priv.h" +#include "util.h" + +#include +#include +#include + +/** + * PhoshDBusNotification: + * + * A notification submitted via the DBus notification interface + * + * The #PhoshDBusNotification is a notification submitted via the + * org.freedesktop.Notification interface. + */ + +typedef struct _PhoshDBusNotification { + PhoshNotification parent; + + GCancellable *cancel; +} PhoshDBusNotification; + + +G_DEFINE_TYPE (PhoshDBusNotification, phosh_dbus_notification, PHOSH_TYPE_NOTIFICATION) + + +static void +on_app_activated (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + g_autoptr (PhoshDBusNotification) self = user_data; + g_autoptr (GError) err = NULL; + GAppInfo *info; + gboolean success; + + info = phosh_notification_get_app_info (PHOSH_NOTIFICATION (self)); + success = phosh_util_activate_action_finish (res, &err); + if (!success) { + g_warning ("Failed to activate %s: %s", g_app_info_get_id (info), err->message); + return; + } + + g_debug ("Activated '%s'", g_app_info_get_id (info)); +} + + +static void +phosh_dbus_notification_do_action (PhoshNotification *notification, guint id, const char *action) +{ + PhoshDBusNotification *self = PHOSH_DBUS_NOTIFICATION (notification); + PhoshNotifyManager *nm = phosh_notify_manager_get_default (); + GAppInfo *info; + + info = phosh_notification_get_app_info (notification); + if (info) { + phosh_util_activate_action (info, + NULL, + NULL, + self->cancel, + on_app_activated, + g_object_ref (self)); + } + phosh_dbus_notifications_emit_action_invoked (PHOSH_DBUS_NOTIFICATIONS (nm), id, action); +} + + +static void +phosh_dbus_notification_finalize (GObject *object) +{ + PhoshDBusNotification *self = PHOSH_DBUS_NOTIFICATION (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + G_OBJECT_CLASS (phosh_dbus_notification_parent_class)->finalize (object); +} + + +static void +phosh_dbus_notification_class_init (PhoshDBusNotificationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshNotificationClass *notification_class = PHOSH_NOTIFICATION_CLASS (klass); + + object_class->finalize = phosh_dbus_notification_finalize; + + notification_class->do_action = phosh_dbus_notification_do_action; +} + + +static void +phosh_dbus_notification_init (PhoshDBusNotification *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshDBusNotification * +phosh_dbus_notification_new (guint id, + const char *app_name, + GAppInfo *info, + const char *summary, + const char *body, + GIcon *icon, + GIcon *image, + PhoshNotificationUrgency urgency, + GStrv actions, + gboolean transient, + gboolean resident, + const char *category, + const char *profile, + GDateTime *timestamp) +{ + return g_object_new (PHOSH_TYPE_DBUS_NOTIFICATION, + "id", id, + "summary", summary, + "body", body, + "app-name", app_name, + "app-icon", icon, + /* Set info after fallback name and icon */ + "app-info", info, + "image", image, + "urgency", urgency, + "actions", actions, + "transient", transient, + "resident", resident, + "category", category, + "profile", profile, + "timestamp", timestamp, + NULL); +} diff --git a/src/notifications/dbus-notification.h b/src/notifications/dbus-notification.h new file mode 100644 index 000000000..a742ef507 --- /dev/null +++ b/src/notifications/dbus-notification.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_DBUS_NOTIFICATION phosh_dbus_notification_get_type () + +G_DECLARE_FINAL_TYPE (PhoshDBusNotification, phosh_dbus_notification, + PHOSH, DBUS_NOTIFICATION, PhoshNotification) + +PhoshDBusNotification *phosh_dbus_notification_new (guint id, + const char *app_name, + GAppInfo *info, + const char *summary, + const char *body, + GIcon *icon, + GIcon *image, + PhoshNotificationUrgency urgency, + GStrv actions, + gboolean transient, + gboolean resident, + const char *category, + const char *profile, + GDateTime *timestamp); + +G_END_DECLS diff --git a/src/notifications/meson.build b/src/notifications/meson.build new file mode 100644 index 000000000..383fce8ed --- /dev/null +++ b/src/notifications/meson.build @@ -0,0 +1,29 @@ +phosh_notifications_inc = include_directories('.') + +phosh_notifications_headers = files( + 'dbus-notification.h', + 'mount-notification.h', + 'notification-banner.h', + 'notification-content.h', + 'notification-frame.h', + 'notification-list.h', + 'notification-source.h', + 'notification.h', + 'notify-feedback.h', + 'notify-manager.h', + 'timestamp-label.h', +) + +phosh_notifications_sources = files( + 'dbus-notification.c', + 'mount-notification.c', + 'notification-banner.c', + 'notification-content.c', + 'notification-frame.c', + 'notification-list.c', + 'notification-source.c', + 'notification.c', + 'notify-feedback.c', + 'notify-manager.c', + 'timestamp-label.c', +) diff --git a/src/notifications/mount-notification.c b/src/notifications/mount-notification.c new file mode 100644 index 000000000..af79f5e3b --- /dev/null +++ b/src/notifications/mount-notification.c @@ -0,0 +1,154 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-mount-notification" + +#include "phosh-config.h" +#include "mount-notification.h" +#include "shell-priv.h" + +#include +#include +#include + +/** + * PhoshMountNotification: + * + * A notification shown when a device got mounted + * + * The #PhoshMountNotification is responsible for showing the necessary + * information when a device got mounted and providing the open action. + */ + +typedef struct _PhoshMountNotification { + PhoshNotification parent; + + GCancellable *cancellable; +} PhoshMountNotification; + + +G_DEFINE_TYPE (PhoshMountNotification, phosh_mount_notification, PHOSH_TYPE_NOTIFICATION) + + +static void +on_launch_finished (GAppInfo *source, + GAsyncResult *result, + PhoshMountNotification *self) +{ + g_autoptr (GError) err = NULL; + + if (!g_app_info_launch_uris_finish (source, result, &err)) { + g_warning ("Failed to open %s: %s", + phosh_notification_get_summary (PHOSH_NOTIFICATION (self)), + err->message); + } + + g_object_unref (self); +} + + +static void +phosh_mount_notification_do_action (PhoshNotification *notification, guint id, const char *action) +{ + PhoshMountNotification *self = PHOSH_MOUNT_NOTIFICATION (notification); + g_autoptr (GAppInfo) info = g_app_info_get_default_for_type ("inode/directory", FALSE); + g_autoptr (GList) l = NULL; + g_autoptr (GdkAppLaunchContext) context = NULL; + + g_debug ("Action %s for %d", action, id); + + if (!G_IS_APP_INFO (info)) { + g_warning ("No handler for inode/directory"); + return; + } + + l = g_list_append (l, (gpointer)action); + context = phosh_shell_get_app_launch_context (phosh_shell_get_default ()); + self->cancellable = g_cancellable_new (); + g_app_info_launch_uris_async (info, + l, + G_APP_LAUNCH_CONTEXT (context), + self->cancellable, + (GAsyncReadyCallback)on_launch_finished, + g_object_ref (self)); +} + + +static void +phosh_mount_notification_dispose (GObject *object) +{ + PhoshMountNotification *self = PHOSH_MOUNT_NOTIFICATION (object); + + g_clear_pointer (&self->cancellable, g_cancellable_cancel); + + G_OBJECT_CLASS (phosh_mount_notification_parent_class)->dispose (object); +} + + +static void +phosh_mount_notification_class_init (PhoshMountNotificationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshNotificationClass *notification_class = PHOSH_NOTIFICATION_CLASS (klass); + + object_class->dispose = phosh_mount_notification_dispose; + notification_class->do_action = phosh_mount_notification_do_action; +} + + +static void +phosh_mount_notification_init (PhoshMountNotification *self) +{ +} + + +PhoshMountNotification * +phosh_mount_notification_new_from_mount (guint id, GMount *mount) +{ + g_autofree char *name = NULL; + g_autofree char *uri = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GIcon) app_icon = NULL; + g_autoptr (GFile) root = NULL; + g_autoptr (GAppInfo) info = NULL; + g_autoptr (GAppInfo) handler_info = NULL; + GDesktopAppInfo *desktop_info; + char *actions[] = { NULL, _("Open"), NULL }; + + name = g_mount_get_name (mount); + g_debug ("Mount '%s' added", name); + icon = g_mount_get_symbolic_icon (mount); + root = g_mount_get_root (mount); + + handler_info = g_app_info_get_default_for_type ("inode/directory", FALSE); + /* Only add an action if we have a handler */ + if (handler_info) { + uri = g_file_get_uri (root); + if (uri) + actions[0] = uri; + } + + app_icon = g_themed_icon_new ("applications-system-symbolic"); + desktop_info = g_desktop_app_info_new (PHOSH_APP_ID ".desktop"); + if (desktop_info) { + info = G_APP_INFO (desktop_info); + } + + return g_object_new (PHOSH_TYPE_MOUNT_NOTIFICATION, + "id", id, + "summary", name, + "app-info", info, + "app-icon", app_icon, + "image", icon, + "urgency", PHOSH_NOTIFICATION_URGENCY_NORMAL, + "actions", uri ? actions : NULL, + "transient", FALSE, + "resident", FALSE, + "category", "device.added", + NULL); +} diff --git a/src/notifications/mount-notification.h b/src/notifications/mount-notification.h new file mode 100644 index 000000000..84ace91aa --- /dev/null +++ b/src/notifications/mount-notification.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_MOUNT_NOTIFICATION phosh_mount_notification_get_type () + +G_DECLARE_FINAL_TYPE (PhoshMountNotification, phosh_mount_notification, + PHOSH, MOUNT_NOTIFICATION, PhoshNotification) + +PhoshMountNotification *phosh_mount_notification_new_from_mount (guint id, GMount *mount); + +G_END_DECLS diff --git a/src/notifications/notification-banner.c b/src/notifications/notification-banner.c new file mode 100644 index 000000000..ce7a9a524 --- /dev/null +++ b/src/notifications/notification-banner.c @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-notification-banner" + +#include "phosh-config.h" +#include "animation.h" +#include "layersurface-priv.h" +#include "notification-banner.h" +#include "notification-frame.h" +#include "shell-priv.h" +#include "util.h" + +#include + +#define BANNER_MIN_WIDTH 360 + +/** + * PhoshNotificationBanner: + * + * A floating notification + */ + +enum { + PROP_0, + PROP_NOTIFICATION, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +struct _PhoshNotificationBanner { + PhoshLayerSurface parent; + + PhoshNotification *notification; + gulong handler_expired; + gulong handler_closed; + + gboolean slide_up; + PhoshAnimation *animation; +}; +typedef struct _PhoshNotificationBanner PhoshNotificationBanner; + + +G_DEFINE_TYPE (PhoshNotificationBanner, phosh_notification_banner, PHOSH_TYPE_LAYER_SURFACE) + + +static void +clear_handler (PhoshNotificationBanner *self) +{ + g_clear_signal_handler (&self->handler_expired, self->notification); + g_clear_signal_handler (&self->handler_closed, self->notification); +} + + +static void +phosh_notification_banner_slide (double value, gpointer user_data) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (user_data); + int margin, height; + + gtk_window_get_size (GTK_WINDOW (self), NULL, &height); + margin = -(height * 0.9) * (self->slide_up ? value : (1.0 - value)); + + phosh_layer_surface_set_margins (PHOSH_LAYER_SURFACE (self), margin, 0, 0, 0); + + phosh_layer_surface_wl_surface_commit (PHOSH_LAYER_SURFACE (self)); +} + + +static void +phosh_notification_banner_slide_done (gpointer user_data) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (user_data); + + g_clear_pointer (&self->animation, phosh_animation_unref); + if (self->slide_up) + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +static void +expired (PhoshNotification *notification, + PhoshNotificationBanner *self) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_BANNER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + clear_handler (self); + + if (gtk_widget_get_mapped (GTK_WIDGET (self))) { + self->slide_up = TRUE; + self->animation = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + 250 * PHOSH_ANIMATION_SLOWDOWN, + PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC, + phosh_notification_banner_slide, + phosh_notification_banner_slide_done, + self); + phosh_animation_start (self->animation); + } else { + gtk_widget_destroy (GTK_WIDGET (self)); + } +} + + +static void +closed (PhoshNotification *notification, + PhoshNotificationReason reason, + PhoshNotificationBanner *self) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_BANNER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + clear_handler (self); + + /* Close the banner */ + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +static void +phosh_notification_banner_set_notification (PhoshNotificationBanner *self, + PhoshNotification *notification) +{ + GtkWidget *content; + + g_set_object (&self->notification, notification); + + content = phosh_notification_frame_new (TRUE, NULL); + phosh_notification_frame_bind_notification (PHOSH_NOTIFICATION_FRAME (content), + self->notification); + gtk_container_add (GTK_CONTAINER (self), content); + + self->handler_expired = g_signal_connect (self->notification, "expired", + G_CALLBACK (expired), self); + self->handler_closed = g_signal_connect (self->notification, "closed", + G_CALLBACK (closed), self); +} + + +static void +phosh_notification_banner_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (object); + + switch (property_id) { + case PROP_NOTIFICATION: + phosh_notification_banner_set_notification (self, + g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_banner_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (object); + + switch (property_id) { + case PROP_NOTIFICATION: + g_value_set_object (value, self->notification); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_banner_dispose (GObject *object) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (object); + + clear_handler (self); + + g_clear_pointer (&self->animation, phosh_animation_unref); + g_clear_object (&self->notification); + + G_OBJECT_CLASS (phosh_notification_banner_parent_class)->dispose (object); +} + + +static void +phosh_notification_banner_map (GtkWidget *widget) +{ + PhoshNotificationBanner *self = PHOSH_NOTIFICATION_BANNER (widget); + int height; + + GTK_WIDGET_CLASS (phosh_notification_banner_parent_class)->map (widget); + + height = gtk_widget_get_allocated_height (widget); + phosh_layer_surface_set_size (PHOSH_LAYER_SURFACE (widget), -1, height); + + phosh_animation_start (self->animation); +} + + +static void +phosh_notification_banner_class_init (PhoshNotificationBannerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_notification_banner_dispose; + object_class->set_property = phosh_notification_banner_set_property; + object_class->get_property = phosh_notification_banner_get_property; + + widget_class->map = phosh_notification_banner_map; + + /** + * PhoshNotificationBanner:notification: + * + * The #PhoshNotification shown + */ + props[PROP_NOTIFICATION] = + g_param_spec_object ("notification", "", "", + PHOSH_TYPE_NOTIFICATION, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "phosh-notification-banner"); +} + + +static void +phosh_notification_banner_init (PhoshNotificationBanner *self) +{ + self->animation = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + 250 * PHOSH_ANIMATION_SLOWDOWN, + PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC, + phosh_notification_banner_slide, + phosh_notification_banner_slide_done, + self); +} + + +GtkWidget * +phosh_notification_banner_new (PhoshNotification *notification) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshMonitor *monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + int width = BANNER_MIN_WIDTH; + + phosh_shell_get_usable_area (phosh_shell_get_default (), NULL, NULL, &width, NULL); + + return g_object_new (PHOSH_TYPE_NOTIFICATION_BANNER, + "notification", notification, + "width-request", BANNER_MIN_WIDTH, + "valign", GTK_ALIGN_CENTER, + /* layer surface */ + "layer-shell", phosh_wayland_get_zwlr_layer_shell_v1 (wl), + "wl-output", monitor ? monitor->wl_output : NULL, + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP, + "width", MIN (width, 450), + /* Initial height to not take up the whole screen */ + "height", 100, + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", FALSE, + "exclusive-zone", 0, + "namespace", "phosh notification", + NULL); +} + +/** + * phosh_notification_banner_get_notification + * @self: The banner + * + * Get the notification. + * + * Returns:(transfer none): The notification + */ +PhoshNotification * +phosh_notification_banner_get_notification (PhoshNotificationBanner *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_BANNER (self), NULL); + + return self->notification; +} diff --git a/src/notifications/notification-banner.h b/src/notifications/notification-banner.h new file mode 100644 index 000000000..a55e73771 --- /dev/null +++ b/src/notifications/notification-banner.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "notification.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFICATION_BANNER (phosh_notification_banner_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshNotificationBanner, phosh_notification_banner, PHOSH, NOTIFICATION_BANNER, PhoshLayerSurface) + + +GtkWidget *phosh_notification_banner_new (PhoshNotification *notification); +PhoshNotification *phosh_notification_banner_get_notification (PhoshNotificationBanner *self); + + +G_END_DECLS diff --git a/src/notifications/notification-content.c b/src/notifications/notification-content.c new file mode 100644 index 000000000..6103cd88d --- /dev/null +++ b/src/notifications/notification-content.c @@ -0,0 +1,474 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-notification-content" + +#include "phosh-config.h" +#include "notification-content.h" + +#include +#include + +/** + * PhoshNotificationContent: + * + * Content of a notification + */ + +enum { + PROP_0, + PROP_NOTIFICATION, + PROP_SHOW_BODY, + PROP_ACTION_FILTER_KEYS, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +struct _PhoshNotificationContent { + GtkListBoxRow parent; + + PhoshNotification *notification; + + GtkWidget *msg_body; + GtkWidget *lbl_summary; + GtkWidget *lbl_body; + GtkWidget *img_image; + GtkWidget *box_actions; + + gboolean show_body; + GStrv action_filter_keys; +}; +typedef struct _PhoshNotificationContent PhoshNotificationContent; + + +G_DEFINE_TYPE (PhoshNotificationContent, phosh_notification_content, GTK_TYPE_LIST_BOX_ROW) + + +static gboolean +set_image (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshNotificationContent *self = user_data; + GIcon *image = g_value_get_object (from_value); + + gtk_widget_set_visible (self->img_image, image != NULL); + + g_value_set_object (to_value, image); + + return TRUE; +} + + +static gboolean +set_summary (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshNotificationContent *self = user_data; + const char* summary = g_value_get_string (from_value); + + gtk_widget_set_visible (self->lbl_summary, !gm_str_is_null_or_empty (summary)); + + g_value_set_string (to_value, summary); + + return TRUE; +} + + +static gboolean +set_body (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + PhoshNotificationContent *self = user_data; + const char* body = g_value_get_string (from_value); + gboolean visible; + + visible = body != NULL && g_strcmp0 (body, "") && self->show_body; + gtk_widget_set_visible (self->lbl_body, visible); + + g_value_set_string (to_value, body); + + return TRUE; +} + + +static GStrv +get_action_filter_keys (PhoshNotification *notification, const char * const *action_filter_keys) +{ + GAppInfo *info = phosh_notification_get_app_info (notification); + g_autoptr (GStrvBuilder) filter_builder = NULL; + + if (action_filter_keys == NULL || action_filter_keys[0] == NULL) + return NULL; + + if (info == NULL) + return NULL; + + filter_builder = g_strv_builder_new (); + for (int i = 0; i < g_strv_length ((GStrv)action_filter_keys); i++) { + g_auto (GStrv) f = g_desktop_app_info_get_string_list (G_DESKTOP_APP_INFO (info), + action_filter_keys[i], + NULL); + if (f) + g_strv_builder_addv (filter_builder, (const char **)f); + } + + return g_strv_builder_end (filter_builder); +} + + +static gboolean +action_matches_filter (const char *action, const char *const *filters) +{ + /* Empty filter cannot match */ + if (filters == NULL || filters[0] == NULL) + return TRUE; + + for (int i = 0; i < g_strv_length ((GStrv)filters); i++) { + if (g_str_has_prefix (action, filters[i])) + return TRUE; + } + return FALSE; +} + + + +static void +set_actions (PhoshNotificationContent *self, PhoshNotification *notification) + +{ + GStrv actions; + g_auto (GStrv) filters = NULL; + + gtk_container_foreach (GTK_CONTAINER (self->box_actions), + (GtkCallback) gtk_widget_destroy, + NULL); + + if (notification == NULL) + return; + + actions = phosh_notification_get_actions (notification); + if (actions == NULL) { + return; + } + + filters = get_action_filter_keys (notification, (const char *const *)self->action_filter_keys); + + for (int i = 0; actions[i] != NULL; i += 2) { + GtkWidget *btn; + GtkWidget *lbl; + + /* The default action is already triggered by the notification body */ + if (g_strcmp0 (actions[i], "default") == 0) { + GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self->msg_body)); + gtk_style_context_add_class (context, "phosh-notification-body"); + continue; + } + + if (actions[i + 1] == NULL) { + g_warning ("Expected action label at %i, got NULL", i); + break; + } + + /* Only Filter actions if a filter is set, otherwise it's "use all actions" */ + if (filters != NULL && filters[0] != NULL) { + if (action_matches_filter (actions[i], (const char * const *)filters)) + g_object_set (self, "show-body", TRUE, NULL); + else + continue; + } + + lbl = g_object_new (GTK_TYPE_LABEL, + "label", actions[i + 1], + "xalign", 0.0, + "halign", GTK_ALIGN_CENTER, + "ellipsize", PANGO_ELLIPSIZE_MIDDLE, + "visible", TRUE, + NULL); + + btn = g_object_new (GTK_TYPE_BUTTON, + "action-name", "noti.activate", + "action-target", g_variant_new_string (actions[i]), + "hexpand", TRUE, + "vexpand", TRUE, + "visible", TRUE, + NULL); + gtk_container_add (GTK_CONTAINER (btn), lbl); + + gtk_container_add (GTK_CONTAINER (self->box_actions), btn); + } +} + + +static void +on_actions_changed (PhoshNotification *notification, + GParamSpec *pspec, + PhoshNotificationContent *self) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_CONTENT (self)); + + set_actions (self, notification); +} + + + +static void +phosh_notification_content_set_notification (PhoshNotificationContent *self, + PhoshNotification *notification) +{ + g_set_object (&self->notification, notification); + + /* Use the "transform" function to show/hide when set/unset */ + g_object_bind_property_full (self->notification, "image", + self->img_image, "gicon", + G_BINDING_SYNC_CREATE, + set_image, + NULL, + self, + NULL); + + g_object_bind_property_full (self->notification, "summary", + self->lbl_summary, "label", + G_BINDING_SYNC_CREATE, + set_summary, + NULL, + self, + NULL); + + g_object_bind_property_full (self->notification, "body", + self->lbl_body, "label", + G_BINDING_SYNC_CREATE, + set_body, + NULL, + self, + NULL); + + g_signal_connect_object (self->notification, "notify::actions", + G_CALLBACK (on_actions_changed), self, 0); + set_actions (self, self->notification); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NOTIFICATION]); +} + + +static void +phosh_notification_content_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationContent *self = PHOSH_NOTIFICATION_CONTENT (object); + + switch (property_id) { + case PROP_NOTIFICATION: + phosh_notification_content_set_notification (self, + g_value_get_object (value)); + break; + case PROP_SHOW_BODY: + self->show_body = g_value_get_boolean (value); + break; + case PROP_ACTION_FILTER_KEYS: + self->action_filter_keys = g_value_dup_boxed (value); + set_actions (self, self->notification); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_content_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationContent *self = PHOSH_NOTIFICATION_CONTENT (object); + + switch (property_id) { + case PROP_NOTIFICATION: + g_value_set_object (value, self->notification); + break; + case PROP_SHOW_BODY: + g_value_set_boolean (value, self->show_body); + break; + case PROP_ACTION_FILTER_KEYS: + g_value_set_boxed (value, self->action_filter_keys); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_content_finalize (GObject *object) +{ + PhoshNotificationContent *self = PHOSH_NOTIFICATION_CONTENT (object); + + g_clear_object (&self->notification); + g_clear_pointer (&self->action_filter_keys, g_strfreev); + + G_OBJECT_CLASS (phosh_notification_content_parent_class)->finalize (object); +} + + +static void +phosh_notification_content_class_init (PhoshNotificationContentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_notification_content_finalize; + object_class->set_property = phosh_notification_content_set_property; + object_class->get_property = phosh_notification_content_get_property; + + /** + * PhoshNotificationContent:notification: + * @self: the #PhoshNotificationContent + * + * The #PhoshNotification shown in @self + */ + props[PROP_NOTIFICATION] = + g_param_spec_object ("notification", + "Notification", + "Notification being shown", + PHOSH_TYPE_NOTIFICATION, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshNotificationContent:show-body: + * + * Whether the body of the notification is shown + */ + props[PROP_SHOW_BODY] = + g_param_spec_boolean ("show-body", + "", + "", + TRUE, + G_PARAM_CONSTRUCT | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + /* + * PhoshNotificationContent::action-filter-keys + * + * The keys will be used to look up filter values in the + * applications desktop file. Actions starting with those values + * will be used on the lock screen. + */ + props[PROP_ACTION_FILTER_KEYS] = + g_param_spec_boxed ("action-filter-keys", "", "", + G_TYPE_STRV, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/notification-content.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationContent, msg_body); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationContent, lbl_summary); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationContent, lbl_body); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationContent, img_image); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationContent, box_actions); + + gtk_widget_class_set_css_name (widget_class, "phosh-notification-content"); +} + + +static void +action_activate (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshNotificationContent *self = data; + const char *target; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_CONTENT (self)); + + target = g_variant_get_string (parameter, NULL); + + phosh_notification_activate (self->notification, target); +} + + +static GActionEntry entries[] = { + { .name = "activate", .activate = action_activate, .parameter_type = "s" }, +}; + + +static void +phosh_notification_content_init (PhoshNotificationContent *self) +{ + g_autoptr (GActionMap) map = NULL; + + self->show_body = TRUE; + + map = G_ACTION_MAP (g_simple_action_group_new ()); + g_action_map_add_action_entries (map, + entries, + G_N_ELEMENTS (entries), + self); + gtk_widget_insert_action_group (GTK_WIDGET (self), + "noti", + G_ACTION_GROUP (map)); + + gtk_widget_init_template (GTK_WIDGET (self)); + + + g_object_bind_property (self, + "show-body", + self->lbl_body, + "visible", + G_BINDING_DEFAULT); + + g_object_bind_property (self, + "show-body", + self->box_actions, + "visible", + G_BINDING_DEFAULT); +} + + +GtkWidget * +phosh_notification_content_new (PhoshNotification *notification, + gboolean show_body, + const char * const *action_filter_keys) +{ + return g_object_new (PHOSH_TYPE_NOTIFICATION_CONTENT, + "action-filter-keys", action_filter_keys, + "notification", notification, + "show-body", show_body, + NULL); +} + +/** + * phosh_notification_content_get_notification + * @self: The notification content + * + * Get the notification. + * + * Returns:(transfer none): The notification + */ +PhoshNotification * +phosh_notification_content_get_notification (PhoshNotificationContent *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_CONTENT (self), NULL); + + return self->notification; +} diff --git a/src/notifications/notification-content.h b/src/notifications/notification-content.h new file mode 100644 index 000000000..f84e99574 --- /dev/null +++ b/src/notifications/notification-content.h @@ -0,0 +1,30 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "notification.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFICATION_CONTENT (phosh_notification_content_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshNotificationContent, phosh_notification_content, PHOSH, NOTIFICATION_CONTENT, GtkListBoxRow) + + +GtkWidget *phosh_notification_content_new (PhoshNotification *notification, + gboolean show_body, + const char * const *action_filters); +PhoshNotification *phosh_notification_content_get_notification (PhoshNotificationContent *self); + + +G_END_DECLS diff --git a/src/notifications/notification-frame.c b/src/notifications/notification-frame.c new file mode 100644 index 000000000..b29435a07 --- /dev/null +++ b/src/notifications/notification-frame.c @@ -0,0 +1,618 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-notification-frame" + +#include "phosh-config.h" +#include "notification-content.h" +#include "notification-frame.h" +#include "notification-source.h" +#include "swipe-away-bin.h" +#include "shell-priv.h" +#include "util.h" +#include "timestamp-label.h" + +#include +#include + +/** + * PhoshNotificationFrame: + * + * A frame containing one or more notifications + */ + + +enum { + PROP_0, + PROP_SHOW_BODY, + PROP_ANIMATE_SHOW, + PROP_ACTION_FILTER_KEYS, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +struct _PhoshNotificationFrame { + GtkEventBox parent; + + GListModel *model; + gulong model_watch; + + GBinding *bind_name; + GBinding *bind_icon; + GBinding *bind_timestamp; + + GtkRevealer *revealer; + + GtkWidget *box; + GtkWidget *lbl_app_name; + GtkWidget *img_icon; + GtkWidget *list_notifs; + GtkWidget *updated; + + gboolean show_body; + gboolean animate_show; + GStrv action_filter_keys; + + /* needed so that the gestures aren't immediately destroyed */ + GtkGesture *header_click_gesture; + GtkGesture *list_click_gesture; + + int start_x; + int start_y; + GtkListBoxRow *active_row; +}; +typedef struct _PhoshNotificationFrame PhoshNotificationFrame; + + +G_DEFINE_TYPE (PhoshNotificationFrame, phosh_notification_frame, GTK_TYPE_EVENT_BOX) + + +#define DRAG_THRESHOLD_DISTANCE 16 + + +enum { + SIGNAL_EMPTY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +static void +phosh_notification_frame_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationFrame *self = PHOSH_NOTIFICATION_FRAME (object); + + switch (property_id) { + case PROP_SHOW_BODY: + self->show_body = g_value_get_boolean (value); + break; + case PROP_ANIMATE_SHOW: + phosh_notification_frame_set_animate_show (self, g_value_get_boolean (value)); + break; + case PROP_ACTION_FILTER_KEYS: + self->action_filter_keys = g_value_dup_boxed (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_frame_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationFrame *self = PHOSH_NOTIFICATION_FRAME (object); + + switch (property_id) { + case PROP_SHOW_BODY: + g_value_set_boolean (value, self->show_body); + break; + case PROP_ANIMATE_SHOW: + g_value_set_boolean (value, phosh_notification_frame_get_animate_show (self)); + break; + case PROP_ACTION_FILTER_KEYS: + g_value_set_boxed (value, self->action_filter_keys); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_frame_finalize (GObject *object) +{ + PhoshNotificationFrame *self = PHOSH_NOTIFICATION_FRAME (object); + + /* Don't clear bindings, they're already unref'd before here */ + + g_clear_signal_handler (&self->model_watch, self->model); + + g_clear_object (&self->model); + g_clear_pointer (&self->action_filter_keys, g_strfreev); + + G_OBJECT_CLASS (phosh_notification_frame_parent_class)->finalize (object); +} + + +static gboolean +motion_notify (PhoshNotificationFrame *self, + GdkEventMotion *event) +{ + if (self->start_x >= 0 && self->start_y >= 0) { + int current_x, current_y; + double dx, dy; + + gtk_widget_translate_coordinates (GTK_WIDGET (self->box), + gtk_widget_get_toplevel (GTK_WIDGET (self)), + event->x, event->y, + ¤t_x, ¤t_y); + + dx = current_x - self->start_x; + dy = current_y - self->start_y; + + if (sqrt (dx * dx + dy * dy) > DRAG_THRESHOLD_DISTANCE) { + gtk_gesture_set_state (self->header_click_gesture, GTK_EVENT_SEQUENCE_DENIED); + gtk_gesture_set_state (self->list_click_gesture, GTK_EVENT_SEQUENCE_DENIED); + } + } + + return GDK_EVENT_PROPAGATE; +} + + +static void +pressed (PhoshNotificationFrame *self, + int n_press, + double x, + double y, + GtkGesture *gesture, + GtkGesture *other_gesture) +{ + GdkEventSequence *sequence = + gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture)); + + if (n_press != 1) { + gtk_gesture_set_sequence_state (gesture, sequence, GTK_EVENT_SEQUENCE_DENIED); + + return; + } + + gtk_widget_translate_coordinates (self->box, + gtk_widget_get_toplevel (GTK_WIDGET (self)), + x, y, + &self->start_x, &self->start_y); + + /* When the title row is clicked we proxy it to the first item */ + self->active_row = + gtk_list_box_get_row_at_y (GTK_LIST_BOX (self->list_notifs), + gesture == self->header_click_gesture ? 0 : y); + + gtk_gesture_set_sequence_state (other_gesture, sequence, GTK_EVENT_SEQUENCE_DENIED); +} + +static void +header_pressed (PhoshNotificationFrame *self, + int n_press, + double x, + double y) +{ + pressed (self, n_press, x, y, + self->header_click_gesture, + self->list_click_gesture); +} + + +static void +list_pressed (PhoshNotificationFrame *self, + int n_press, + double x, + double y) +{ + pressed (self, n_press, x, y, + self->list_click_gesture, + self->header_click_gesture); +} + + +static void +released (PhoshNotificationFrame *self, + int n_press, + double x, + double y, + GtkGesture *gesture) +{ + GtkListBoxRow *pressed_row = self->active_row; + GtkListBoxRow *active_row; + PhoshNotificationContent *content; + PhoshNotification *notification; + + /* When the title row is clicked we proxy it to the first item */ + active_row = + gtk_list_box_get_row_at_y (GTK_LIST_BOX (self->list_notifs), + gesture == self->header_click_gesture ? 0 : y); + + self->active_row = NULL; + self->start_x = -1; + self->start_y = -1; + + if (pressed_row != active_row) { + gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED); + + return; + } + + content = PHOSH_NOTIFICATION_CONTENT (active_row); + notification = phosh_notification_content_get_notification (content); + + phosh_notification_activate (notification, + PHOSH_NOTIFICATION_DEFAULT_ACTION); +} + + +static void +notification_activated (PhoshNotificationFrame *self, + GtkListBoxRow *row, + GtkListBox *list) +{ + PhoshNotificationContent *content; + PhoshNotification *notification; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_CONTENT (row)); + + content = PHOSH_NOTIFICATION_CONTENT (row); + notification = phosh_notification_content_get_notification (content); + + phosh_notification_activate (notification, + PHOSH_NOTIFICATION_DEFAULT_ACTION); +} + + +static void +removed (PhoshNotificationFrame *self) +{ + gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), FALSE); +} + + +static void +on_child_revealed_changed (PhoshNotificationFrame *self, + GParamSpec *pspec, + GtkRevealer *revealer) +{ + guint i, n; + + if (gtk_revealer_get_child_revealed (revealer)) + return; + + n = g_list_model_get_n_items (self->model); + for (i = 0; i < n; i++) { + g_autoptr (PhoshNotification) notification = NULL; + + notification = g_list_model_get_item (self->model, 0); + + phosh_notification_close (notification, PHOSH_NOTIFICATION_REASON_CLOSED); + } +} + + +/** + * needs_separator: + * @row: the #GtkListBoxRow + * + * Checks if a visible separator is needed. This is only the case when + * there are no action buttons as otherwise the action buttons are enough + * separation. + * + * Returns: `TRUE` if a separator is needed + */ +static gboolean +needs_separator (PhoshNotificationContent *content) +{ + PhoshNotification *notification; + GStrv actions; + + notification = phosh_notification_content_get_notification (content); + actions = phosh_notification_get_actions (notification); + + if (gm_strv_is_null_or_empty (actions)) + return TRUE; + + if (g_strv_length (actions) == 2 && g_str_equal (actions[0], "default")) + return TRUE; + + return FALSE; +} + + +static void +header_func (GtkListBoxRow *current_row, GtkListBoxRow *prev_row, gpointer user_data) +{ + PhoshNotificationContent *content; + + if (prev_row == NULL) { + gtk_list_box_row_set_header (current_row, NULL); + return; + } + + content = PHOSH_NOTIFICATION_CONTENT (prev_row); + + if (needs_separator (content)) { + GtkWidget *separator = gtk_separator_new (GTK_ORIENTATION_HORIZONTAL); + gtk_widget_set_hexpand (separator, TRUE); + gtk_list_box_row_set_header (current_row, separator); + } else { + gtk_list_box_row_set_header (current_row, NULL); + } +} + + +static void +phosh_notification_frame_class_init (PhoshNotificationFrameClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_notification_frame_finalize; + object_class->set_property = phosh_notification_frame_set_property; + object_class->get_property = phosh_notification_frame_get_property; + + /** + * PhoshNotificationFrame:show-body: + * + * Whether notifications in this frame should show the notification body + */ + props[PROP_SHOW_BODY] = + g_param_spec_boolean ("show-body", + "", + "", + TRUE, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + /** + * PhoshNotificationFrame:animate-show: + * + * Whether to insert a frame with an animation. + */ + props[PROP_ANIMATE_SHOW] = + g_param_spec_boolean ("animate-show", "", "", + TRUE, + G_PARAM_EXPLICIT_NOTIFY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshNotificationFrame:action-filter-keys: + * + * The keys will be used to look up filter values in the + * applications desktop file. Actions starting with those values + * will be used on the lock screen. + */ + props[PROP_ACTION_FILTER_KEYS] = + g_param_spec_boxed ("action-filter-keys", "", "", + G_TYPE_STRV, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[SIGNAL_EMPTY] = g_signal_new ("empty", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); + g_type_ensure (PHOSH_TYPE_TIMESTAMP_LABEL); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/notification-frame.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, revealer); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, box); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, lbl_app_name); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, img_icon); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, list_notifs); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, updated); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, header_click_gesture); + gtk_widget_class_bind_template_child (widget_class, PhoshNotificationFrame, list_click_gesture); + + gtk_widget_class_bind_template_callback (widget_class, motion_notify); + gtk_widget_class_bind_template_callback (widget_class, header_pressed); + gtk_widget_class_bind_template_callback (widget_class, list_pressed); + gtk_widget_class_bind_template_callback (widget_class, released); + gtk_widget_class_bind_template_callback (widget_class, notification_activated); + gtk_widget_class_bind_template_callback (widget_class, removed); + + gtk_widget_class_set_css_name (widget_class, "phosh-notification-frame"); +} + + +static void +phosh_notification_frame_init (PhoshNotificationFrame *self) +{ + self->animate_show = TRUE; + self->show_body = TRUE; + self->start_x = -1; + self->start_y = -1; + + g_type_ensure (PHOSH_TYPE_SWIPE_AWAY_BIN); + + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_list_box_set_header_func (GTK_LIST_BOX (self->list_notifs), + header_func, + self, + NULL); + + g_signal_connect_swapped (self->revealer, "notify::child-revealed", + G_CALLBACK (on_child_revealed_changed), self); +} + + +GtkWidget * +phosh_notification_frame_new (gboolean show_body, const char * const *action_filter_keys) +{ + return g_object_new (PHOSH_TYPE_NOTIFICATION_FRAME, + "show-body", show_body, + "action-filter-keys", action_filter_keys, + NULL); +} + + +static GtkWidget * +create_row (gpointer item, gpointer data) +{ + PhoshNotification *notification = item; + PhoshNotificationFrame *self = PHOSH_NOTIFICATION_FRAME (data); + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self), NULL); + + if (self->action_filter_keys) { + for (int i = 0; i < g_strv_length (self->action_filter_keys); i++) + g_debug ("%s: %s", __func__, self->action_filter_keys[i]); + } + return phosh_notification_content_new (notification, self->show_body, + (const char * const *)self->action_filter_keys); +} + + +static void +items_changed (GListModel *list, + guint position, + guint removed, + guint added, + PhoshNotificationFrame *self) +{ + g_autoptr (PhoshNotification) notification = NULL; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self)); + + /* Disconnect from the last notification (if any) */ + g_clear_object (&self->bind_name); + g_clear_object (&self->bind_icon); + g_clear_object (&self->bind_timestamp); + + /* Get the latest notification in the model */ + notification = g_list_model_get_item (self->model, 0); + + if (notification == NULL) { + /* No first notification means no notifications aka we're empty + * and should be removed from $thing we're in (banner or list) + */ + g_signal_emit (self, signals[SIGNAL_EMPTY], 0); + + return; + } + + /* Bind to the new one */ + self->bind_name = g_object_bind_property (notification, "app-name", + self->lbl_app_name, "label", + G_BINDING_SYNC_CREATE); + + self->bind_icon = g_object_bind_property (notification, "app-icon", + self->img_icon, "gicon", + G_BINDING_SYNC_CREATE); + + self->bind_timestamp = g_object_bind_property (notification, "timestamp", + self->updated, "timestamp", + G_BINDING_SYNC_CREATE); + + gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), TRUE); +} + + +void +phosh_notification_frame_bind_model (PhoshNotificationFrame *self, + GListModel *model) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self)); + g_return_if_fail (G_IS_LIST_MODEL (model)); + g_return_if_fail (g_type_is_a (g_list_model_get_item_type (model), + PHOSH_TYPE_NOTIFICATION)); + + g_set_object (&self->model, model); + + gtk_list_box_bind_model (GTK_LIST_BOX (self->list_notifs), + model, + create_row, + self, + NULL); + + self->model_watch = g_signal_connect (model, "items-changed", + G_CALLBACK (items_changed), self); + items_changed (model, 0, 0, 0, self); +} + + +/** + * phosh_notification_frame_bind_notification: + * @self: the #PhoshNotificationFrame + * @notification: a #PhoshNotification + * + * Helper function for frames that only need to contain a single notification + * + * Wraps phosh_notification_frame_bind_model() by placing @notification in + * a #PhoshNotificationSource + */ +void +phosh_notification_frame_bind_notification (PhoshNotificationFrame *self, + PhoshNotification *notification) +{ + g_autoptr (PhoshNotificationSource) store = NULL; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + store = phosh_notification_source_new ("dummy"); + + phosh_notification_source_add (store, notification); + + phosh_notification_frame_bind_model (self, G_LIST_MODEL (store)); +} + + +const char * const * +phosh_notification_frame_get_action_filter_keys (PhoshNotificationFrame *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self), NULL); + + return (const char *const *)self->action_filter_keys; +} + + +void +phosh_notification_frame_set_animate_show (PhoshNotificationFrame *self, gboolean animate) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self)); + + if (self->animate_show == animate) + return; + + self->animate_show = animate; + + /* Avoid animation when widget isn't mapped yet to save some CPU cycles */ + if (!self->animate_show && !gtk_widget_get_mapped (GTK_WIDGET (self))) + gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), TRUE); +} + + +gboolean +phosh_notification_frame_get_animate_show (PhoshNotificationFrame *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_FRAME (self), FALSE); + + return self->animate_show; +} diff --git a/src/notifications/notification-frame.h b/src/notifications/notification-frame.h new file mode 100644 index 000000000..bda5f3e2b --- /dev/null +++ b/src/notifications/notification-frame.h @@ -0,0 +1,36 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "notification.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFICATION_FRAME (phosh_notification_frame_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshNotificationFrame, phosh_notification_frame, PHOSH, NOTIFICATION_FRAME, GtkEventBox) + + +GtkWidget *phosh_notification_frame_new (gboolean show_body, + const char * const *action_filters); +void phosh_notification_frame_bind_notification (PhoshNotificationFrame *self, + PhoshNotification *notification); +void phosh_notification_frame_bind_model (PhoshNotificationFrame *self, + GListModel *model); +const char* const *phosh_notification_frame_get_action_filter_keys (PhoshNotificationFrame *self); + +void phosh_notification_frame_set_animate_show (PhoshNotificationFrame *self, + gboolean animate); +gboolean phosh_notification_frame_get_animate_show (PhoshNotificationFrame *self); + +G_END_DECLS diff --git a/src/notifications/notification-list.c b/src/notifications/notification-list.c new file mode 100644 index 000000000..c07ec8552 --- /dev/null +++ b/src/notifications/notification-list.c @@ -0,0 +1,322 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-notification-list" + +#include "phosh-config.h" +#include "notification-source.h" +#include "notification-list.h" + +/** + * PhoshNotificationList: + * + * A list containing one or more #PhoshNotificationSource + * + * #PhoshNotificationList maps between #PhoshNotificationSource objects and their + * notifications creating and removing sources on the fly. + */ + + +struct _PhoshNotificationList { + GObject parent; + + /* Ordered list of notification sources */ + GSequence *source_list; + + /* cache for fast get_item */ + struct { + gboolean is_valid; + guint position; + GSequenceIter *iter; + } last; + + /* Map of source name -> iter in source_list */ + GHashTable *source_map; + + /* Map of id -> notification */ + GHashTable *notifications; +}; +typedef struct _PhoshNotificationList PhoshNotificationList; + + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshNotificationList, phosh_notification_list, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +static void +phosh_notification_list_finalize (GObject *object) +{ + PhoshNotificationList *self = PHOSH_NOTIFICATION_LIST (object); + + g_clear_pointer (&self->source_list, g_sequence_free); + g_clear_pointer (&self->source_map, g_hash_table_unref); + + g_clear_pointer (&self->notifications, g_hash_table_unref); + + G_OBJECT_CLASS (phosh_notification_list_parent_class)->finalize (object); +} + + +static void +phosh_notification_list_class_init (PhoshNotificationListClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_notification_list_finalize; +} + + +static GType +list_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_NOTIFICATION_SOURCE; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshNotificationList *self = PHOSH_NOTIFICATION_LIST (list); + GSequenceIter *it = NULL; + + if (self->last.is_valid) { + if (position < G_MAXUINT && self->last.position == position + 1) + it = g_sequence_iter_prev (self->last.iter); + else if (position > 0 && self->last.position == position - 1) + it = g_sequence_iter_next (self->last.iter); + else if (self->last.position == position) + it = self->last.iter; + } + + if (it == NULL) + it = g_sequence_get_iter_at_pos (self->source_list, position); + + self->last.iter = it; + self->last.position = position; + self->last.is_valid = TRUE; + + if (g_sequence_iter_is_end (it)) + return NULL; + else + return g_object_ref (g_sequence_get (it)); +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshNotificationList *self = PHOSH_NOTIFICATION_LIST (list); + + return g_sequence_get_length (self->source_list); +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static void +phosh_notification_list_init (PhoshNotificationList *self) +{ + /* The source list owns its elements */ + self->source_list = g_sequence_new ((GDestroyNotify) g_object_unref); + + /* The source map owns its keys, but values belong to source_list */ + self->source_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + + /* The notifications are owened but the source they are in */ + self->notifications = g_hash_table_new_full (g_direct_hash, + g_direct_equal, + NULL, + NULL); +} + + +/** + * phosh_notification_list_new: + * + * Create an empty #PhoshNotificationList, generally used via + * phosh_notify_manager_get_list() + * + * Returns: the new #PhoshNotificationList + */ +PhoshNotificationList * +phosh_notification_list_new (void) +{ + return g_object_new (PHOSH_TYPE_NOTIFICATION_LIST, NULL); +} + + +/** + * empty: + * @source: the #PhoshNotificationSource + * @self: the #PhoshNotificationList that owns @source + * + * The @source now contains no items, remove it from @self + */ +static void +empty (PhoshNotificationSource *source, + PhoshNotificationList *self) +{ + int i = 0; + GSequenceIter *iter; + const char *source_id; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_LIST (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION_SOURCE (source)); + + source_id = phosh_notification_source_get_name (source); + + iter = g_hash_table_lookup (self->source_map, source_id); + + i = g_sequence_iter_get_position (iter); + + g_sequence_remove (iter); + + g_hash_table_remove (self->source_map, source_id); + + self->last.is_valid = FALSE; + self->last.position = 0; + self->last.iter = NULL; + + g_list_model_items_changed (G_LIST_MODEL (self), i, 1, 0); +} + + +/** + * closed: + * @notification: the #PhoshNotification + * @reason: the #PhoshNotificationReason @notification closed + * @self: the #PhoshNotificationList the @notification is (indirectly) in + * + * @notification has been closed, remove it from the notification map + * + * NOTE: We don't remove it from it's #PhoshNotificationSource, that's left + * for the source to handle + */ +static void +closed (PhoshNotification *notification, + PhoshNotificationReason reason, + PhoshNotificationList *self) +{ + guint id; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_LIST (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + id = phosh_notification_get_id (notification); + + g_hash_table_remove (self->notifications, GUINT_TO_POINTER (id)); +} + + +/** + * phosh_notification_list_add: + * @self: the #PhoshNotificationList + * @source_id: id of the #PhoshNotificationSource @notification belongs to + * (may not currently exist) + * @notification: the new #PhoshNotification + * + * Registers a new notification with @self adding to (or creating) the relevant + * #PhoshNotificationSource + */ +void +phosh_notification_list_add (PhoshNotificationList *self, + const char *source_id, + PhoshNotification *notification) +{ + PhoshNotificationSource *source; + GSequenceIter *source_iter; + guint id; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_LIST (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + id = phosh_notification_get_id (notification); + + g_hash_table_insert (self->notifications, + GUINT_TO_POINTER (id), + notification); + + /* Lookup an existing entry for source id */ + source_iter = g_hash_table_lookup (self->source_map, source_id); + + if (source_iter == NULL) { + /* Source doesn't currently exist, generate it */ + source = phosh_notification_source_new (source_id); + g_signal_connect (source, "empty", G_CALLBACK (empty), self); + + /* Add to the start, iter remains valid as long as the item exist */ + source_iter = g_sequence_prepend (self->source_list, source); + + g_hash_table_insert (self->source_map, g_strdup (source_id), source_iter); + + self->last.is_valid = FALSE; + self->last.iter = NULL; + self->last.position = 0; + + /* We added an item to the start */ + g_list_model_items_changed (G_LIST_MODEL (self), 0, 0, 1); + } else if (!g_sequence_iter_is_begin (source_iter)) { + int old_pos; + + source = g_sequence_get (source_iter); + + old_pos = g_sequence_iter_get_position (source_iter); + + /* Move the source to the top of the list */ + g_sequence_move (source_iter, + g_sequence_get_begin_iter (self->source_list)); + + self->last.is_valid = FALSE; + self->last.iter = NULL; + self->last.position = 0; + + /* We "removed" an item */ + g_list_model_items_changed (G_LIST_MODEL (self), old_pos, 1, 0); + /* And "added" it to the start */ + g_list_model_items_changed (G_LIST_MODEL (self), 0, 0, 1); + } else { + source = g_sequence_get (source_iter); + } + + phosh_notification_source_add (source, notification); + + g_signal_connect (notification, "closed", G_CALLBACK (closed), self); +} + + +/** + * phosh_notification_list_get_by_id: + * @self: the #PhoshNotificationList + * @id: the #PhoshNotification:id to lookup + * + * Find a #PhoshNotification in @self by it's @id + * + * Returns:(nullable)(transfer none): the #PhoshNotification or %NULL + */ +PhoshNotification * +phosh_notification_list_get_by_id (PhoshNotificationList *self, + guint id) +{ + PhoshNotification *notification = NULL; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_LIST (self), NULL); + + notification = g_hash_table_lookup (self->notifications, + GUINT_TO_POINTER (id)); + + return notification; +} diff --git a/src/notifications/notification-list.h b/src/notifications/notification-list.h new file mode 100644 index 000000000..5bd136e10 --- /dev/null +++ b/src/notifications/notification-list.h @@ -0,0 +1,31 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "notification.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFICATION_LIST (phosh_notification_list_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshNotificationList, phosh_notification_list, PHOSH, NOTIFICATION_LIST, GObject) + + +PhoshNotificationList *phosh_notification_list_new (void); +void phosh_notification_list_add (PhoshNotificationList *self, + const char *source_id, + PhoshNotification *notification); +PhoshNotification *phosh_notification_list_get_by_id (PhoshNotificationList *self, + guint id); + +G_END_DECLS diff --git a/src/notifications/notification-source.c b/src/notifications/notification-source.c new file mode 100644 index 000000000..e2b3fc0e1 --- /dev/null +++ b/src/notifications/notification-source.c @@ -0,0 +1,265 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-notification-source" + +#include "phosh-config.h" +#include "notification-source.h" + +/** + * PhoshNotificationSource: + * + * A #GListModel containing one or more notifications + * + * A #PhoshNotificationSource groups notifications. A source has a name + * which is usually the app_id of the sending application. + */ + + +typedef struct _PhoshNotificationSource { + GObject parent; + + GListStore *list; + + char *name; +} PhoshNotificationSource; + + +static void list_iface_init (GListModelInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshNotificationSource, phosh_notification_source, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_LIST_MODEL, list_iface_init)) + + +enum { + PROP_0, + PROP_NAME, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +enum { + SIGNAL_EMPTY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +static void +phosh_notification_source_finalize (GObject *object) +{ + PhoshNotificationSource *self = PHOSH_NOTIFICATION_SOURCE (object); + + g_clear_object (&self->list); + g_clear_pointer (&self->name, g_free); + + G_OBJECT_CLASS (phosh_notification_source_parent_class)->finalize (object); +} + + +static void +phosh_notification_source_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationSource *self = PHOSH_NOTIFICATION_SOURCE (object); + + switch (property_id) { + case PROP_NAME: + self->name = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_source_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotificationSource *self = PHOSH_NOTIFICATION_SOURCE (object); + + switch (property_id) { + case PROP_NAME: + g_value_set_string (value, self->name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_source_class_init (PhoshNotificationSourceClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_notification_source_finalize; + object_class->set_property = phosh_notification_source_set_property; + object_class->get_property = phosh_notification_source_get_property; + + props[PROP_NAME] = + g_param_spec_string ( + "name", + "Name", + "Source name (ID)", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[SIGNAL_EMPTY] = g_signal_new ("empty", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + + +static GType +list_get_item_type (GListModel *list) +{ + return PHOSH_TYPE_NOTIFICATION; +} + + +static gpointer +list_get_item (GListModel *list, guint position) +{ + PhoshNotificationSource *self = PHOSH_NOTIFICATION_SOURCE (list); + + return g_list_model_get_item (G_LIST_MODEL (self->list), position); +} + + +static unsigned int +list_get_n_items (GListModel *list) +{ + PhoshNotificationSource *self = PHOSH_NOTIFICATION_SOURCE (list); + + return g_list_model_get_n_items (G_LIST_MODEL (self->list)); +} + + +static void +list_iface_init (GListModelInterface *iface) +{ + iface->get_item_type = list_get_item_type; + iface->get_item = list_get_item; + iface->get_n_items = list_get_n_items; +} + + +static void +items_changed (GListModel *list, + guint position, + guint removed, + guint added, + PhoshNotificationSource *self) +{ + g_autoptr (PhoshNotification) item = NULL; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_SOURCE (self)); + + g_list_model_items_changed (G_LIST_MODEL (self), position, removed, added); + + item = g_list_model_get_item (G_LIST_MODEL (self->list), 0); + + if (item == NULL) { + g_signal_emit (self, signals[SIGNAL_EMPTY], 0); + } +} + + +static void +phosh_notification_source_init (PhoshNotificationSource *self) +{ + self->list = g_list_store_new (PHOSH_TYPE_NOTIFICATION); + + g_signal_connect (self->list, + "items-changed", + G_CALLBACK (items_changed), + self); +} + + +PhoshNotificationSource * +phosh_notification_source_new (const char *name) +{ + return g_object_new (PHOSH_TYPE_NOTIFICATION_SOURCE, + "name", name, + NULL); +} + + +static void +closed (PhoshNotificationSource *self, + PhoshNotificationReason reason, + PhoshNotification *notification) +{ + int i = 0; + gpointer item = NULL; + gboolean found = FALSE; + + g_return_if_fail (PHOSH_IS_NOTIFICATION_SOURCE (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + while ((item = g_list_model_get_item (G_LIST_MODEL (self->list), i))) { + if (item == notification) { + g_list_store_remove (self->list, i); + g_clear_object (&item); + found = TRUE; + g_clear_object (&item); + break; + } + g_clear_object (&item); + i++; + g_clear_object (&item); + } + + if (!found) { + g_critical ("Can't remove unknown notification %p", notification); + } +} + + +void +phosh_notification_source_add (PhoshNotificationSource *self, + PhoshNotification *notification) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION_SOURCE (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + g_list_store_insert (self->list, 0, notification); + + g_signal_connect_object (notification, + "closed", + G_CALLBACK (closed), + self, + G_CONNECT_SWAPPED); +} + + +const char * +phosh_notification_source_get_name (PhoshNotificationSource *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFICATION_SOURCE (self), NULL); + + return self->name; +} diff --git a/src/notifications/notification-source.h b/src/notifications/notification-source.h new file mode 100644 index 000000000..91054da76 --- /dev/null +++ b/src/notifications/notification-source.h @@ -0,0 +1,29 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#pragma once + +#include + +#include "notification.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFICATION_SOURCE (phosh_notification_source_get_type ()) + + +G_DECLARE_FINAL_TYPE (PhoshNotificationSource, phosh_notification_source, PHOSH, NOTIFICATION_SOURCE, GObject) + + +PhoshNotificationSource *phosh_notification_source_new (const char *name); +void phosh_notification_source_add (PhoshNotificationSource *self, + PhoshNotification *notification); +const char *phosh_notification_source_get_name (PhoshNotificationSource *self); + +G_END_DECLS diff --git a/src/notifications/notification.c b/src/notifications/notification.c new file mode 100644 index 000000000..1fe5631a7 --- /dev/null +++ b/src/notifications/notification.c @@ -0,0 +1,1134 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-notification" + +#include "phosh-config.h" +#include "notification.h" +#include "phosh-enums.h" +#include "app-grid-button.h" + +#include + +/** + * PhoshNotification: + * + * A notification displayed to the user + * + * A notification usually consists of a summary, a body (the content) + * and an icon. + */ + +enum { + PROP_0, + PROP_ID, + PROP_APP_NAME, + PROP_SUMMARY, + PROP_BODY, + PROP_APP_ICON, + PROP_APP_INFO, + PROP_IMAGE, + PROP_URGENCY, + PROP_ACTIONS, + PROP_TRANSIENT, + PROP_RESIDENT, + PROP_CATEGORY, + PROP_PROFILE, + PROP_TIMESTAMP, + PROP_SOUND_FILE, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +enum { + SIGNAL_ACTIONED, + SIGNAL_EXPIRED, + SIGNAL_CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + + +typedef struct _PhoshNotificationPrivate { + guint id; + char *app_name; + GDateTime *updated; + char *summary; + char *body; + GIcon *icon; + GIcon *image; + GAppInfo *info; + PhoshNotificationUrgency urgency; + GStrv actions; + gboolean transient; + gboolean resident; + char *category; + char *profile; + char *sound_file; + + guint timeout; +} PhoshNotificationPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshNotification, phosh_notification, G_TYPE_OBJECT) + + +static void +phosh_notification_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotification *self = PHOSH_NOTIFICATION (object); + + switch (property_id) { + case PROP_ID: + phosh_notification_set_id (self, g_value_get_uint (value)); + break; + case PROP_APP_NAME: + phosh_notification_set_app_name (self, g_value_get_string (value)); + break; + case PROP_TIMESTAMP: + phosh_notification_set_timestamp (self, g_value_get_boxed (value)); + break; + case PROP_SUMMARY: + phosh_notification_set_summary (self, g_value_get_string (value)); + break; + case PROP_BODY: + phosh_notification_set_body (self, g_value_get_string (value)); + break; + case PROP_APP_ICON: + phosh_notification_set_app_icon (self, g_value_get_object (value)); + break; + case PROP_APP_INFO: + phosh_notification_set_app_info (self, g_value_get_object (value)); + break; + case PROP_IMAGE: + phosh_notification_set_image (self, g_value_get_object (value)); + break; + case PROP_URGENCY: + phosh_notification_set_urgency (self, g_value_get_enum (value)); + break; + case PROP_ACTIONS: + phosh_notification_set_actions (self, g_value_get_boxed (value)); + break; + case PROP_TRANSIENT: + phosh_notification_set_transient (self, g_value_get_boolean (value)); + break; + case PROP_RESIDENT: + phosh_notification_set_resident (self, g_value_get_boolean (value)); + break; + case PROP_CATEGORY: + phosh_notification_set_category (self, g_value_get_string (value)); + break; + case PROP_PROFILE: + phosh_notification_set_profile (self, g_value_get_string (value)); + break; + case PROP_SOUND_FILE: + phosh_notification_set_sound_file (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotification *self = PHOSH_NOTIFICATION (object); + + switch (property_id) { + case PROP_ID: + g_value_set_uint (value, phosh_notification_get_id (self)); + break; + case PROP_APP_NAME: + g_value_set_string (value, phosh_notification_get_app_name (self)); + break; + case PROP_TIMESTAMP: + g_value_set_boxed (value, phosh_notification_get_timestamp (self)); + break; + case PROP_SUMMARY: + g_value_set_string (value, phosh_notification_get_summary (self)); + break; + case PROP_BODY: + g_value_set_string (value, phosh_notification_get_body (self)); + break; + case PROP_APP_ICON: + g_value_set_object (value, phosh_notification_get_app_icon (self)); + break; + case PROP_APP_INFO: + g_value_set_object (value, phosh_notification_get_app_info (self)); + break; + case PROP_IMAGE: + g_value_set_object (value, phosh_notification_get_image (self)); + break; + case PROP_URGENCY: + g_value_set_enum (value, phosh_notification_get_urgency (self)); + break; + case PROP_ACTIONS: + g_value_set_boxed (value, phosh_notification_get_actions (self)); + break; + case PROP_TRANSIENT: + g_value_set_boolean (value, phosh_notification_get_transient (self)); + break; + case PROP_RESIDENT: + g_value_set_boolean (value, phosh_notification_get_resident (self)); + break; + case PROP_CATEGORY: + g_value_set_string (value, phosh_notification_get_category (self)); + break; + case PROP_PROFILE: + g_value_set_string (value, phosh_notification_get_profile (self)); + break; + case PROP_SOUND_FILE: + g_value_set_string (value, phosh_notification_get_sound_file (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notification_finalize (GObject *object) +{ + PhoshNotification *self = PHOSH_NOTIFICATION (object); + PhoshNotificationPrivate *priv = phosh_notification_get_instance_private (self); + + /* If we've been dismissed cancel the auto timeout */ + if (priv->timeout != 0) { + g_source_remove (priv->timeout); + } + + g_clear_pointer (&priv->app_name, g_free); + g_clear_pointer (&priv->updated, g_date_time_unref); + g_clear_pointer (&priv->summary, g_free); + g_clear_pointer (&priv->body, g_free); + g_clear_object (&priv->icon); + g_clear_object (&priv->image); + g_clear_object (&priv->info); + g_clear_pointer (&priv->actions, g_strfreev); + g_clear_pointer (&priv->category, g_free); + g_clear_pointer (&priv->profile, g_free); + g_clear_pointer (&priv->sound_file, g_free); + + G_OBJECT_CLASS (phosh_notification_parent_class)->finalize (object); +} + + +static void +phosh_notification_class_init (PhoshNotificationClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_notification_finalize; + object_class->set_property = phosh_notification_set_property; + object_class->get_property = phosh_notification_get_property; + + props[PROP_ID] = + g_param_spec_uint ( + "id", + "ID", + "Notification ID", + 0, + G_MAXUINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_APP_NAME] = + g_param_spec_string ( + "app-name", + "App Name", + "The applications's name", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TIMESTAMP] = + g_param_spec_boxed ( + "timestamp", + "Timestamp", + "The time that notification came in.", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_SUMMARY] = + g_param_spec_string ( + "summary", + "Summary", + "The notification's summary", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_BODY] = + g_param_spec_string ( + "body", + "Body", + "The notification's body", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_APP_ICON] = + g_param_spec_object ( + "app-icon", + "App Icon", + "Application icon", + G_TYPE_ICON, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshNotification:app-info: + * + * When non-%NULL this overrides the values of [property@Notification:app-name] + * and [property@Notification:app-icon] with those from the `GAppInfo`. + */ + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", "", "", + G_TYPE_APP_INFO, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_IMAGE] = + g_param_spec_object ( + "image", + "Image", + "Notification image", + G_TYPE_ICON, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_URGENCY] = + g_param_spec_enum ( + "urgency", + "Urgency", + "Notification urgency", + PHOSH_TYPE_NOTIFICATION_URGENCY, + PHOSH_NOTIFICATION_URGENCY_NORMAL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_ACTIONS] = + g_param_spec_boxed ( + "actions", + "Actions", + "Notification actions", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_TRANSIENT] = + g_param_spec_boolean ( + "transient", + "Transient", + "The notification is transient", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_RESIDENT] = + g_param_spec_boolean ( + "resident", + "Resident", + "The notification is resident", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_CATEGORY] = + g_param_spec_string ( + "category", + "Category", + "The notification's category", + "", + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshNotification:profile: + * + * The feedback profile to use for the event triggered by this + * notification. Valid values from Feedbackd's Feedback Theme + * Specification as well as the value `none` meaning: don't trigger + * any feedback for this event. If `NULL` (the default) the decision + * is left to feedbackd. + */ + props[PROP_PROFILE] = + g_param_spec_string ("profile", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshNotification:sound-file: + * + * The sound file to play. + */ + props[PROP_SOUND_FILE] = + g_param_spec_string ("sound-file", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + /** + * PhoshNotification::actioned: + * @self: The notification + * @action: The name of the activated action + * + * Emitted when the user activates one of the provided actions + * + * This includes the default action. + */ + signals[SIGNAL_ACTIONED] = g_signal_new ("actioned", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + G_TYPE_STRING); + /** + * PhoshNotification::expired: + * @self: The notification + * + * Emitted when the timeout set by [method@Notification.expires] has expired + */ + signals[SIGNAL_EXPIRED] = g_signal_new ("expired", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); + /** + * PhoshNotification::closed: + * @self: The #PhoshNotifiation + * @reason: Why @self was closed + * + * Emitted when the notification has been closed + */ + signals[SIGNAL_CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 1, + PHOSH_TYPE_NOTIFICATION_REASON); +} + + +static void +phosh_notification_init (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv = phosh_notification_get_instance_private (self); + + priv->app_name = g_strdup (_("Notification")); +} + + +PhoshNotification * +phosh_notification_new (guint id, + const char *app_name, + GAppInfo *info, + const char *summary, + const char *body, + GIcon *icon, + GIcon *image, + PhoshNotificationUrgency urgency, + GStrv actions, + gboolean transient, + gboolean resident, + const char *category, + const char *profile, + GDateTime *timestamp) +{ + + return g_object_new (PHOSH_TYPE_NOTIFICATION, + "id", id, + "summary", summary, + "body", body, + "app-name", app_name, + "app-icon", icon, + /* Set info after fallback name and icon */ + "app-info", info, + "image", image, + "urgency", urgency, + "actions", actions, + "transient", transient, + "resident", resident, + "category", category, + "profile", profile, + "timestamp", timestamp, + NULL); +} + + +void +phosh_notification_set_id (PhoshNotification *self, + guint id) +{ + PhoshNotificationPrivate *priv; + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (priv->id == id) { + return; + } + + priv->id = id; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ID]); +} + + +guint +phosh_notification_get_id (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->id; +} + + +void +phosh_notification_set_app_icon (PhoshNotification *self, + GIcon *icon) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + g_clear_object (&priv->icon); + if (icon != NULL) { + priv->icon = g_object_ref (icon); + } else { + priv->icon = g_themed_icon_new (PHOSH_APP_UNKNOWN_ICON); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_ICON]); +} + +/** + * phosh_notification_get_app_icon: + * @self: The notification: + * + * Returns:(transfer none): The notification's icon + */ +GIcon * +phosh_notification_get_app_icon (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + if (priv->info && g_app_info_get_icon (priv->info)) { + return g_app_info_get_icon (priv->info); + } + + return priv->icon; +} + + +void +phosh_notification_set_app_info (PhoshNotification *self, + GAppInfo *info) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + g_clear_object (&priv->info); + + if (info != NULL) { + GIcon *icon; + const char *name; + + priv->info = g_object_ref (info); + + icon = g_app_info_get_icon (info); + name = g_app_info_get_name (info); + + phosh_notification_set_app_icon (self, icon); + phosh_notification_set_app_name (self, name); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_INFO]); +} + +/** + * phosh_notification_get_app_info: + * @self: The notification: + * + * Returns:(transfer none): The notification's app info + */ +GAppInfo * +phosh_notification_get_app_info (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->info; +} + + +void +phosh_notification_set_image (PhoshNotification *self, + GIcon *image) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_set_object (&priv->image, image)) { + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IMAGE]); + } +} + +/** + * phosh_notification_get_image: + * @self: The notification: + * + * Returns:(transfer none): The notification's image + */ +GIcon * +phosh_notification_get_image (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->image; +} + + +void +phosh_notification_set_summary (PhoshNotification *self, + const char *summary) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->summary, summary) == 0) { + return; + } + + g_clear_pointer (&priv->summary, g_free); + priv->summary = g_strdup (summary); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SUMMARY]); +} + + +const char * +phosh_notification_get_summary (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->summary; +} + + +void +phosh_notification_set_body (PhoshNotification *self, + const char *body) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->body, body) == 0) { + return; + } + + g_clear_pointer (&priv->body, g_free); + priv->body = g_strdup (body); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BODY]); +} + + +const char * +phosh_notification_get_body (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->body; +} + + + +void +phosh_notification_set_app_name (PhoshNotification *self, + const char *app_name) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->app_name, app_name) == 0) { + return; + } + + g_clear_pointer (&priv->app_name, g_free); + + if (app_name && + strlen (app_name) > 0 && + g_strcmp0 (app_name, "notify-send") != 0) { + priv->app_name = g_strdup (app_name); + } else { + priv->app_name = g_strdup (_("Notification")); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_APP_NAME]); +} + + +const char * +phosh_notification_get_app_name (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + if (priv->info && g_app_info_get_name (priv->info)) { + return g_app_info_get_name (priv->info); + } + + return priv->app_name; +} + +/** + * phosh_notification_set_timestamp: + * @self: A #PhoshNotification + * @timestamp: (nullable): A timestamp or %NULL + * + * Sets the timestamp of a notification. If %NULL is passed it's set + * to the current date and time. + */ +void +phosh_notification_set_timestamp (PhoshNotification *self, + GDateTime *timestamp) +{ + PhoshNotificationPrivate *priv; + g_autoptr (GDateTime) now = NULL; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (timestamp == NULL) { + now = g_date_time_new_now_local (); + timestamp = now; + } + + if (priv->updated != NULL && g_date_time_compare (priv->updated, timestamp) == 0) + return; + + g_clear_pointer (&priv->updated, g_date_time_unref); + priv->updated = g_date_time_ref (timestamp); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIMESTAMP]); +} + + +GDateTime * +phosh_notification_get_timestamp (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), NULL); + priv = phosh_notification_get_instance_private (self); + + return priv->updated; +} + + +void +phosh_notification_set_actions (PhoshNotification *self, + GStrv actions) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + g_clear_pointer (&priv->actions, g_strfreev); + priv->actions = g_strdupv (actions); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIONS]); +} + +/** + * phosh_notification_get_actions: + * @self: The #PhoshNotification + * + * Returns: (transfer none): The notification's actions + */ +GStrv +phosh_notification_get_actions (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), 0); + priv = phosh_notification_get_instance_private (self); + + return priv->actions; +} + + +void +phosh_notification_set_urgency (PhoshNotification *self, + PhoshNotificationUrgency urgency) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (priv->urgency == urgency) + return; + + priv->urgency = urgency; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_URGENCY]); +} + + +PhoshNotificationUrgency +phosh_notification_get_urgency (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), + PHOSH_NOTIFICATION_URGENCY_NORMAL); + priv = phosh_notification_get_instance_private (self); + + return priv->urgency; +} + +/** + * phosh_notification_set_transient: + * @self: the #PhoshNotification + * @transient: if @self is transient + * + * Set if @self should go to the message tray + */ +void +phosh_notification_set_transient (PhoshNotification *self, + gboolean transient) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (priv->transient == transient) + return; + + priv->transient = transient; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSIENT]); +} + +/** + * phosh_notification_get_transient: + * @self: the #PhoshNotification + * + * Transient notifications don't go to the message tray + * + * Returns: %TRUE when transient, otherwise %FALSE + */ +gboolean +phosh_notification_get_transient (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), TRUE); + priv = phosh_notification_get_instance_private (self); + + return priv->transient; +} + +/** + * phosh_notification_set_resident: + * @self: the #PhoshNotification + * @resident: is the notification resident + * + * Set whether or not invoking actions dismiss @self + */ +void +phosh_notification_set_resident (PhoshNotification *self, + gboolean resident) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (priv->resident == resident) + return; + + priv->resident = resident; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIDENT]); +} + +/** + * phosh_notification_get_resident: + * @self: the #PhoshNotification + * + * When %TRUE invoking an action _doesn't_ dismiss the notification + * + * Returns: %TRUE when resident, otherwise %FALSE + */ +gboolean +phosh_notification_get_resident (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), FALSE); + priv = phosh_notification_get_instance_private (self); + + return priv->resident; +} + +/** + * phosh_notification_set_category: + * @self: the #PhoshNotification + * @category: the new category + * + * Set the category of the notification, such as `email.arrived` + */ +void +phosh_notification_set_category (PhoshNotification *self, + const char *category) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->category, category) == 0) + return; + + g_clear_pointer (&priv->category, g_free); + priv->category = g_strdup (category); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CATEGORY]); +} + +/** + * phosh_notification_get_category: + * @self: the #PhoshNotification + * + * Get the category hint the notification was sent with + * + * See + * + * Returns: the category or %NULL + */ +const char * +phosh_notification_get_category (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), NULL); + priv = phosh_notification_get_instance_private (self); + + return priv->category; +} + +/** + * phosh_notification_set_profile: + * @self: the #PhoshNotification + * @profile: the feedback profile to use + * + * Set the feedback profile (constrained by feedbackd's + * global policy) for the event related to this notification. + */ +void +phosh_notification_set_profile (PhoshNotification *self, + const char *profile) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->profile, profile) == 0) + return; + + g_clear_pointer (&priv->profile, g_free); + priv->profile = g_strdup (profile); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PROFILE]); +} + +/** + * phosh_notification_get_profile: + * @self: the #PhoshNotification + * + * Get the intended feedback profile for the event related to this + * notification + * + * See the Feedback Theme Specification for details. + * + * Returns: the profile or %NULL + */ +const char * +phosh_notification_get_profile (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), NULL); + priv = phosh_notification_get_instance_private (self); + + return priv->profile; +} + +/** + * phosh_notification_set_sound_file: + * @self: the #PhoshNotification + * @sound_file: the sound file to use + * + * Set the sound file to play (constrained by feedbackd's global + * policy) + */ +void +phosh_notification_set_sound_file (PhoshNotification *self, + const char *sound_file) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + if (g_strcmp0 (priv->sound_file, sound_file) == 0) + return; + + g_clear_pointer (&priv->sound_file, g_free); + priv->sound_file = g_strdup (sound_file); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SOUND_FILE]); +} + +/** + * phosh_notification_get_sound_file: + * @self: the #PhoshNotification + * + * Get the sound file for the event related to this notification + * + * Returns: the sound file or %NULL + */ +const char * +phosh_notification_get_sound_file (PhoshNotification *self) +{ + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), NULL); + priv = phosh_notification_get_instance_private (self); + + return priv->sound_file; +} + +void +phosh_notification_activate (PhoshNotification *self, + const char *action) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + + g_signal_emit (self, signals[SIGNAL_ACTIONED], 0, action); +} + + +static gboolean +expired (gpointer data) +{ + PhoshNotification *self = data; + PhoshNotificationPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (self), G_SOURCE_REMOVE); + priv = phosh_notification_get_instance_private (self); + + g_debug ("%i expired", priv->id); + + priv->timeout = 0; + + g_signal_emit (self, signals[SIGNAL_EXPIRED], 0); + + return G_SOURCE_REMOVE; +} + +/** + * phosh_notification_expires: + * @self: the #PhoshNotification + * @timeout: delay (in milliseconds) + * + * Set @self to expire after @timeout (from this call) + * + * Note: doesn't close the notification, for that call + * [method@Notification.close] in response to [signal@Notification::expired]. + */ +void +phosh_notification_expires (PhoshNotification *self, + int timeout) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + g_return_if_fail (timeout > 0); + priv = phosh_notification_get_instance_private (self); + + g_clear_handle_id (&priv->timeout, g_source_remove); + priv->timeout = g_timeout_add (timeout, expired, self); + g_source_set_name_by_id (priv->timeout, "[phosh] notification_expires_id"); +} + +/** + * phosh_notification_close: + * @self: the #PhoshNotification + * @reason: Why the notification is closing + */ +void +phosh_notification_close (PhoshNotification *self, + PhoshNotificationReason reason) +{ + PhoshNotificationPrivate *priv; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + priv = phosh_notification_get_instance_private (self); + + /* No point running the timeout, we're already closing */ + if (priv->timeout != 0) { + g_source_remove (priv->timeout); + priv->timeout = 0; + } + + g_signal_emit (self, signals[SIGNAL_CLOSED], 0, reason); +} + +void +phosh_notification_do_action (PhoshNotification *self, guint id, const char *action) +{ + g_return_if_fail (PHOSH_IS_NOTIFICATION (self)); + + if (!PHOSH_NOTIFICATION_GET_CLASS (self)->do_action) + return; + + PHOSH_NOTIFICATION_GET_CLASS (self)->do_action (self, id, action); +} diff --git a/src/notifications/notification.h b/src/notifications/notification.h new file mode 100644 index 000000000..54864e2dd --- /dev/null +++ b/src/notifications/notification.h @@ -0,0 +1,133 @@ +/* + * Copyright © 2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/** + * PhoshNotificationUrgency: + * @PHOSH_NOTIFICATION_URGENCY_LOW: low urgency + * @PHOSH_NOTIFICATION_URGENCY_NORMAL: normal urgency + * @PHOSH_NOTIFICATION_URGENCY_CRITICAL: critical urgency + */ +typedef enum { + PHOSH_NOTIFICATION_URGENCY_LOW = 0, + PHOSH_NOTIFICATION_URGENCY_NORMAL, + PHOSH_NOTIFICATION_URGENCY_CRITICAL, +} PhoshNotificationUrgency; + +/** + * PhoshNotificationReason: + * @PHOSH_NOTIFICATION_REASON_EXPIRED: notification expired + * @PHOSH_NOTIFICATION_REASON_DISMISSED: notification was dismissed + * @PHOSH_NOTIFICATION_REASON_CLOSED: notification was closed + * @PHOSH_NOTIFICATION_REASON_UNDEFINED: undefined reason +*/ +typedef enum { + PHOSH_NOTIFICATION_REASON_EXPIRED = 1, + PHOSH_NOTIFICATION_REASON_DISMISSED = 2, + PHOSH_NOTIFICATION_REASON_CLOSED = 3, + PHOSH_NOTIFICATION_REASON_UNDEFINED = 4, +} PhoshNotificationReason; + + +#define PHOSH_NOTIFICATION_DEFAULT_ACTION "default" +#define PHOSH_NOTIFICATION_DEFAULT_TIMEOUT 5000 /* ms */ + +#define PHOSH_TYPE_NOTIFICATION (phosh_notification_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshNotification, phosh_notification, PHOSH, NOTIFICATION, GObject) + +/** + * PhoshNotificationClass: + * @parent_class: The object class structure needs to be the first + * element in the widget class structure in order for the class mechanism + * to work correctly. This allows a PhoshNotificationClass pointer to be cast to + * a GObjectClass pointer. + * @do_action: This function allows the notification to implement their own + * action behaviour instead of the default DBus interface. + */ +struct _PhoshNotificationClass +{ + GObjectClass parent_class; + + void (*do_action)(PhoshNotification *self, guint id, const char *action); +}; + + +PhoshNotification *phosh_notification_new (guint id, + const char *app_name, + GAppInfo *info, + const char *summary, + const char *body, + GIcon *icon, + GIcon *image, + PhoshNotificationUrgency urgency, + GStrv actions, + gboolean transient, + gboolean resident, + const char *category, + const char *profile, + GDateTime *timestamp); +void phosh_notification_set_id (PhoshNotification *self, + guint id); +guint phosh_notification_get_id (PhoshNotification *self); +void phosh_notification_set_summary (PhoshNotification *self, + const char *summary); +const char *phosh_notification_get_summary (PhoshNotification *self); +void phosh_notification_set_body (PhoshNotification *self, + const char *body); +const char *phosh_notification_get_body (PhoshNotification *self); +void phosh_notification_set_app_name (PhoshNotification *self, + const char *app_name); +const char *phosh_notification_get_app_name (PhoshNotification *self); +GDateTime *phosh_notification_get_timestamp (PhoshNotification *self); +void phosh_notification_set_timestamp (PhoshNotification *self, + GDateTime *timestamp); +void phosh_notification_set_app_icon (PhoshNotification *self, + GIcon *icon); +GIcon *phosh_notification_get_app_icon (PhoshNotification *self); +void phosh_notification_set_app_info (PhoshNotification *self, + GAppInfo *info); +GAppInfo *phosh_notification_get_app_info (PhoshNotification *self); +void phosh_notification_set_image (PhoshNotification *self, + GIcon *icon); +GIcon *phosh_notification_get_image (PhoshNotification *self); +void phosh_notification_set_urgency (PhoshNotification *self, + PhoshNotificationUrgency urgency); +PhoshNotificationUrgency phosh_notification_get_urgency (PhoshNotification *self); +void phosh_notification_set_actions (PhoshNotification *self, + GStrv actions); +GStrv phosh_notification_get_actions (PhoshNotification *self); +void phosh_notification_set_transient (PhoshNotification *self, + gboolean transient); +gboolean phosh_notification_get_transient (PhoshNotification *self); +void phosh_notification_set_resident (PhoshNotification *self, + gboolean resident); +gboolean phosh_notification_get_resident (PhoshNotification *self); +void phosh_notification_set_category (PhoshNotification *self, + const char *category); +const char *phosh_notification_get_profile (PhoshNotification *self); +void phosh_notification_set_profile (PhoshNotification *self, + const char *profile); +const char *phosh_notification_get_sound_file (PhoshNotification *self); +void phosh_notification_set_sound_file (PhoshNotification *self, + const char *sound_file); +const char *phosh_notification_get_category (PhoshNotification *self); +void phosh_notification_activate (PhoshNotification *self, + const char *action); +void phosh_notification_expires (PhoshNotification *self, + int timeout); +void phosh_notification_close (PhoshNotification *self, + PhoshNotificationReason reason); +void phosh_notification_do_action (PhoshNotification *notification, + guint id, + const char *action); + +G_END_DECLS diff --git a/src/notifications/notify-feedback.c b/src/notifications/notify-feedback.c new file mode 100644 index 000000000..7b93a334f --- /dev/null +++ b/src/notifications/notify-feedback.c @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2021-2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-notify-feedback" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "notify-feedback.h" +#include "notification-source.h" +#include "util.h" + +#include + +#include + +/** + * PhoshNotifyFeedback: + * + * Provide feedback on notifications + * + * #PhoshNotifyFeedback is the manager object responsible to provide + * proper feedback on new notifications or when notifications are + * being closed. + */ + +enum { + PROP_0, + PROP_NOTIFICATION_LIST, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshNotifyFeedback { + GObject parent; + + LfbEvent *active_event; + LfbEvent *inactive_event; + PhoshNotificationList *list; + + GSettings *settings; + PhoshNotifyScreenWakeupFlags wakeup_flags; + GStrv wakeup_categories; + PhoshNotificationUrgency wakeup_min_urgency; +}; +G_DEFINE_TYPE (PhoshNotifyFeedback, phosh_notify_feedback, G_TYPE_OBJECT) + +static void +end_notify_feedback (PhoshNotifyFeedback *self) +{ + g_return_if_fail (lfb_is_initted ()); + + if (self->active_event && lfb_event_get_state (self->active_event) == LFB_EVENT_STATE_RUNNING) + lfb_event_end_feedback_async (self->active_event, NULL, NULL, NULL); + + if (self->inactive_event && lfb_event_get_state (self->inactive_event) == LFB_EVENT_STATE_RUNNING) + lfb_event_end_feedback_async (self->inactive_event, NULL, NULL, NULL); +} + +/** + * find_event_inactive: + * @category: The category to look up the event for + * + * Look up an event when for a notification category when the device + * is not in active use. + * + * Returns:(nullable): The event name + */ +static const char * +find_event_inactive (const char *category) +{ + const char *ret = "notification-missed-generic"; + + if (gm_str_is_null_or_empty (category)) + return ret; + + if (g_str_equal (category, "email.arrived")) { + ret = "message-missed-email"; + } else if (g_str_equal (category, "im.received")) { + ret = "message-missed-instant"; + } else if (g_str_equal (category, "x-phosh.sms.received")) { + ret = "message-missed-sms"; + } else if (g_str_equal (category, "x-gnome.call.unanswered")) { + ret = "phone-missed-call"; + } else if (g_str_equal (category, "call.unanswered")) { + ret = "phone-missed-call"; + } + + return ret; +} + +/** + * find_event_active: + * @category: The category to look up the event for + * @important:(out): Whether the event is considered important + * + * Look up an event when for a notification category when the device + * is in active use. + * + * Returns:(nullable): The event name + */ +static const char * +find_event_active (const char *category, gboolean *important) +{ + const char *ret = "notification-new-generic"; + + if (gm_str_is_null_or_empty (category)) + return ret; + + if (g_str_equal (category, "email.arrived")) + ret = "message-new-email"; + else if (g_str_equal (category, "im.received")) + ret = "message-new-instant"; + else if (g_str_equal (category, "x-phosh.sms.received")) + ret = "message-new-sms"; + else if (g_str_equal (category, "x-gnome.call.unanswered")) + ret = "phone-missed-call"; + else if (g_str_equal (category, "call.ended")) + ret = "phone-hangup"; + else if (g_str_equal (category, "call.incoming")) + ret = "phone-incoming-call"; + else if (g_str_equal (category, "call.unanswered")) + ret = "phone-missed-call"; + else if (g_str_has_prefix (category, "x-phosh-cellbroadcast.")) { + ret = "message-new-cellbroadcast"; + if (important) + *important = TRUE; + } + + return ret; +} + + +static void +maybe_wakeup_screen (PhoshNotifyFeedback *self, PhoshNotificationSource *source, guint position, guint num) +{ + g_return_if_fail (num > 0); + + if (self->wakeup_flags & PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_ANY) { + phosh_shell_activate_action (phosh_shell_get_default (), "screensaver.wakeup-screen", NULL); + return; + } + + for (int i = 0; i < num; i++) { + g_autoptr (PhoshNotification) noti = g_list_model_get_item (G_LIST_MODEL (source), position + i); + gboolean wakeup; + + g_return_if_fail (PHOSH_IS_NOTIFICATION (noti)); + + wakeup = phosh_notify_feedback_check_screen_wakeup (self, noti); + if (wakeup) { + phosh_shell_activate_action (phosh_shell_get_default (), "screensaver.wakeup-screen", NULL); + break; + } + } +} + +/** + * maybe_trigger_feedback: + * @self: The notification feedback handler + * @source: The notification source model + * @position: Position where notifications were added to the model + * @num: How many notifications were added to the model + * @inactive_only: Whether to only look for events used when the device + * is considered inactive + * + * Look up events matching the categories of newly added notifications + * and trigger feedback therefore. + * + * Returns: `TRUE` when any feedback was triggered + */ +static gboolean +maybe_trigger_feedback (PhoshNotifyFeedback *self, + PhoshNotificationSource *source, + guint position, + guint num, + gboolean inactive_only) +{ + PhoshShell *shell = phosh_shell_get_default (); + gboolean inactive = phosh_shell_get_blanked (shell) || phosh_shell_get_locked (shell); + + /* Get us the first notification that triggers meaningful feedback */ + for (int i = 0; i < num; i++) { + g_autoptr (PhoshNotification) noti = g_list_model_get_item (G_LIST_MODEL (source), position + i); + const char *category, *profile, *sound_file; + g_autofree char *app_id = NULL; + GAppInfo *info = NULL; + gboolean ret = FALSE; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (noti), FALSE); + g_return_val_if_fail (lfb_is_initted (), FALSE); + + info = phosh_notification_get_app_info (noti); + if (info) + app_id = phosh_strip_suffix_from_app_id (g_app_info_get_id (info)); + + profile = phosh_notification_get_profile (noti); + if (g_strcmp0 (profile, "none") == 0) + continue; + + category = phosh_notification_get_category (noti); + sound_file = phosh_notification_get_sound_file (noti); + + /* The default event */ + if (!inactive_only) { + const char *name; + gboolean important = FALSE; + + name = find_event_active (category, &important); + if (name) { + g_autoptr (LfbEvent) event = event = lfb_event_new (name); + + lfb_event_set_feedback_profile (event, profile); + if (sound_file) + lfb_event_set_sound_file (event, sound_file); + + if (app_id) + lfb_event_set_app_id (event, app_id); + g_debug ("Emitting event %s for %s, profile: %s", name, app_id ?: "unknown", profile); + if (important && app_id) + lfb_event_set_important (event, TRUE); + lfb_event_trigger_feedback_async (event, NULL, NULL, NULL); + /* TODO: we should better track that on the notification */ + g_set_object (&self->active_event, event); + ret = TRUE; + } + } + + /* The event when the device is not in active use (usually long running LED feedback) */ + if (inactive) { + const char *name; + + name = find_event_inactive (category); + if (name) { + g_autoptr (LfbEvent) event = event = lfb_event_new (name); + + lfb_event_set_feedback_profile (event, profile); + if (app_id) + lfb_event_set_app_id (event, app_id); + g_debug ("Emitting event %s for %s, profile: %s", name, app_id ?: "unknown", profile); + lfb_event_trigger_feedback_async (event, NULL, NULL, NULL); + /* TODO: we should better track that on the notification */ + g_set_object (&self->inactive_event, event); + ret = TRUE; + } + } + + if (ret) + return ret; + } + + return FALSE; +} + + +static void +on_notification_source_items_changed (PhoshNotifyFeedback *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + if (!added) + return; + + maybe_wakeup_screen (self, PHOSH_NOTIFICATION_SOURCE (list), position, added); + + maybe_trigger_feedback (self, PHOSH_NOTIFICATION_SOURCE (list), position, added, FALSE); +} + + +static void +on_settings_changed (PhoshNotifyFeedback *self, char *key, GSettings *settings) +{ + self->wakeup_flags = g_settings_get_flags (settings, "wakeup-screen-triggers"); + self->wakeup_min_urgency = g_settings_get_enum (settings, "wakeup-screen-urgency"); + g_strfreev (self->wakeup_categories); + self->wakeup_categories = g_settings_get_strv (settings, "wakeup-screen-categories"); +} + + +static void +on_notification_list_items_changed (PhoshNotifyFeedback *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + g_autoptr (PhoshNotificationSource) first = g_list_model_get_item (list, 0); + + if (!first) { + g_debug ("Notification list empty, ending feedback"); + end_notify_feedback (self); + } + + /* No need to worry about removed sources, signals get detached due + * to g_signal_connect_object () */ + for (int i = position; i < position + added; i++) { + g_autoptr (PhoshNotificationSource) source = g_list_model_get_item (list, position); + /* Listen to new notification on the store for feedback triggering */ + g_signal_connect_object (source, + "items-changed", + G_CALLBACK (on_notification_source_items_changed), + self, + G_CONNECT_SWAPPED); + on_notification_source_items_changed (self, 0, 0, + g_list_model_get_n_items (G_LIST_MODEL (source)), + G_LIST_MODEL (source)); + } +} + + +static void +on_shell_state_changed (PhoshNotifyFeedback *self, GParamSpec *pspec, PhoshShell *shell) +{ + g_return_if_fail (PHOSH_IS_NOTIFY_FEEDBACK (self)); + + /* Feedback ongoing, nothing to do */ + if (self->inactive_event && lfb_event_get_state (self->inactive_event) == LFB_EVENT_STATE_RUNNING) + return; + + if (!phosh_shell_get_blanked (shell) && !phosh_shell_get_locked (shell)) + return; + + /* Ensure we have visual feedback when the device blanks with open notifications */ + for (guint i = 0; i < g_list_model_get_n_items (G_LIST_MODEL (self->list)); i++) { + guint n_items; + g_autoptr (PhoshNotificationSource) source = g_list_model_get_item (G_LIST_MODEL (self->list), i); + + n_items = g_list_model_get_n_items (G_LIST_MODEL (source)); + if (maybe_trigger_feedback (self, source, 0, n_items, TRUE)) + break; + } +} + + +static void +phosh_notify_feedback_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshNotifyFeedback *self = PHOSH_NOTIFY_FEEDBACK (object); + + switch (property_id) { + case PROP_NOTIFICATION_LIST: + self->list = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notify_feedback_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshNotifyFeedback *self = PHOSH_NOTIFY_FEEDBACK (object); + + switch (property_id) { + case PROP_NOTIFICATION_LIST: + g_value_set_object (value, self->list); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_notify_feedback_constructed (GObject *object) +{ + PhoshNotifyFeedback *self = PHOSH_NOTIFY_FEEDBACK (object); + + G_OBJECT_CLASS (phosh_notify_feedback_parent_class)->constructed (object); + + g_signal_connect_swapped (self->list, + "items-changed", + G_CALLBACK (on_notification_list_items_changed), + self); + + g_signal_connect_object (phosh_shell_get_default (), + "notify::shell-state", + G_CALLBACK (on_shell_state_changed), + self, + G_CONNECT_SWAPPED); +} + + +static void +phosh_notify_feedback_dispose (GObject *object) +{ + PhoshNotifyFeedback *self = PHOSH_NOTIFY_FEEDBACK (object); + + g_clear_object (&self->settings); + + if (self->inactive_event && self->active_event) { + end_notify_feedback (self); + g_clear_object (&self->active_event); + g_clear_object (&self->inactive_event); + } + + g_signal_handlers_disconnect_by_data (self->list, self); + g_clear_object (&self->list); + + g_clear_pointer (&self->wakeup_categories, g_strfreev); + + G_OBJECT_CLASS (phosh_notify_feedback_parent_class)->dispose (object); +} + + +static void +phosh_notify_feedback_class_init (PhoshNotifyFeedbackClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_notify_feedback_get_property; + object_class->set_property = phosh_notify_feedback_set_property; + object_class->constructed = phosh_notify_feedback_constructed; + object_class->dispose = phosh_notify_feedback_dispose; + + /** + * PhoshNotifyFeedback:notification-list + * + * The list of notifications that drives the feedback emission + */ + props[PROP_NOTIFICATION_LIST] = + g_param_spec_object ("notification-list", + "", + "", + PHOSH_TYPE_NOTIFICATION_LIST, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_notify_feedback_init (PhoshNotifyFeedback *self) +{ + self->settings = g_settings_new ("sm.puri.phosh.notifications"); + + g_signal_connect_swapped (self->settings, "changed", G_CALLBACK (on_settings_changed), self); + on_settings_changed (self, NULL, self->settings); +} + + +PhoshNotifyFeedback * +phosh_notify_feedback_new (PhoshNotificationList *list) +{ + return PHOSH_NOTIFY_FEEDBACK (g_object_new (PHOSH_TYPE_NOTIFY_FEEDBACK, + "notification-list", list, + NULL)); +} + + +/** + * phosh_notify_feedback_check_screen_wakeup: + * @self: The notification feedback manager + * @notification: The notification to check + * + * Checks if the given notification should trigger screeen wakeup + * + * Returns: %TRUE if the notification should trigger the screen wakeup. + */ +gboolean +phosh_notify_feedback_check_screen_wakeup (PhoshNotifyFeedback *self, + PhoshNotification *notification) +{ + const char *category; + PhoshNotificationUrgency urgency; + + g_return_val_if_fail (PHOSH_IS_NOTIFICATION (notification), FALSE); + + urgency = phosh_notification_get_urgency (notification); + if (self->wakeup_flags & PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_URGENCY && + urgency >= self->wakeup_min_urgency) { + return TRUE; + } + + category = phosh_notification_get_category (notification); + if (!gm_str_is_null_or_empty (category) && + self->wakeup_flags & PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_CATEGORY) { + /* exact match of setting to notification's category (e.g. `im` == `im` or `im.foo == `im.foo` */ + if (g_strv_contains ((const char * const *)self->wakeup_categories, category)) + return TRUE; + + /* setting (`im`) matches the class of the notification (`im.received`) */ + for (int j = 0; j < g_strv_length (self->wakeup_categories); j++) { + if (strchr (self->wakeup_categories[j], '.')) + continue; + + if (g_str_has_prefix (category, self->wakeup_categories[j]) && + category [strlen (self->wakeup_categories[j])] == '.') { + return TRUE; + } + } + } + return FALSE; +} diff --git a/src/notifications/notify-feedback.h b/src/notifications/notify-feedback.h new file mode 100644 index 000000000..75494b472 --- /dev/null +++ b/src/notifications/notify-feedback.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "notification-list.h" + +#include + +#include "phosh-settings-enums.h" + +G_BEGIN_DECLS + + +#define PHOSH_TYPE_NOTIFY_FEEDBACK (phosh_notify_feedback_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshNotifyFeedback, phosh_notify_feedback, PHOSH, NOTIFY_FEEDBACK, GObject) + +PhoshNotifyFeedback *phosh_notify_feedback_new (PhoshNotificationList *list); +gboolean phosh_notify_feedback_check_screen_wakeup (PhoshNotifyFeedback *self, + PhoshNotification *notification); + +G_END_DECLS diff --git a/src/notifications/notify-manager.c b/src/notifications/notify-manager.c new file mode 100644 index 000000000..91db7b243 --- /dev/null +++ b/src/notifications/notify-manager.c @@ -0,0 +1,1027 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-notify-manager" + +#include "phosh-config.h" + +#include + +#include "dbus-notification.h" +#include "notification-banner.h" +#include "notification-list.h" +#include "notify-manager.h" +#include "notify-feedback.h" +#include "shell-priv.h" +#include "phosh-enums.h" +#include "util.h" + +#include + +#define NOTIFICATIONS_KEY_APP_CHILDREN "application-children" + +#define NOTIFICATIONS_APP_SCHEMA_ID PHOSH_NOTIFICATIONS_SCHEMA_ID ".application" +#define NOTIFICATIONS_APP_PREFIX "/org/gnome/desktop/notifications/application" +#define NOTIFICATIONS_APP_KEY_SHOW_BANNERS "show-banners" +#define NOTIFICATIONS_APP_KEY_APP_ID "application-id" +#define NOTIFICATIONS_APP_KEY_ENABLE "enable" + +#define NOTIFICATIONS_SPEC_VERSION "1.2" + +/** + * PhoshNotifyManager: + * + * Manages notifications + * + * #PhoshNotifyManager manages notifications sent from the shell + * itself and via the org.freedesktop.Notification DBus interface. + * See https://developer.gnome.org/notification-spec/ + * + * It maintains a list of notifications via a #PhoshNotificationList. + */ + +#define NOTIFY_DBUS_NAME "org.freedesktop.Notifications" + +enum { + NEW_NOTIFICATION, + NOTIFICATION_ACTIVATED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct _PhoshNotifyManager +{ + PhoshDBusNotificationsSkeleton parent; + + int dbus_name_id; + guint next_id; + guint unknown_source; + gboolean show_banners; + GStrv app_children; + + GSettings *settings; + + /* Notification to be handled on unlock */ + struct { + PhoshNotification *notification; + char *action; + } unlock_notify; + + PhoshNotificationList *list; + PhoshNotifyFeedback *feedback; +} PhoshNotifyManager; + +static void phosh_notify_manager_notify_iface_init (PhoshDBusNotificationsIface *iface); +G_DEFINE_TYPE_WITH_CODE (PhoshNotifyManager, + phosh_notify_manager, + PHOSH_DBUS_TYPE_NOTIFICATIONS_SKELETON, + G_IMPLEMENT_INTERFACE (PHOSH_DBUS_TYPE_NOTIFICATIONS, + phosh_notify_manager_notify_iface_init)); + + +static gboolean +handle_close_notification (PhoshDBusNotifications *skeleton, + GDBusMethodInvocation *invocation, + guint arg_id) +{ + PhoshNotification *notification = NULL; + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + g_debug ("DBus call CloseNotification %u", arg_id); + + notification = phosh_notification_list_get_by_id (self->list, arg_id); + + /* + * ignore errors when closing non-existent notification, at least qt 5.11 is not + * happy about it. + */ + if (notification && PHOSH_IS_NOTIFICATION (notification)) { + phosh_notification_close (notification, PHOSH_NOTIFICATION_REASON_CLOSED); + } else { + phosh_dbus_notifications_emit_notification_closed (PHOSH_DBUS_NOTIFICATIONS (self), + arg_id, + PHOSH_NOTIFICATION_REASON_CLOSED); + } + + phosh_dbus_notifications_complete_close_notification (skeleton, invocation); + + return TRUE; +} + + +static gboolean +handle_get_capabilities (PhoshDBusNotifications *skeleton, GDBusMethodInvocation *invocation) +{ + const char *const capabilities[] = { + "body", "body-markup", "actions", "icon-static", "sound", NULL, + }; + + g_debug ("DBus call GetCapabilities"); + phosh_dbus_notifications_complete_get_capabilities (skeleton, invocation, capabilities); + return TRUE; +} + + +static gboolean +handle_get_server_information (PhoshDBusNotifications *skeleton, GDBusMethodInvocation *invocation) +{ + g_debug ("DBus call GetServerInformation"); + phosh_dbus_notifications_complete_get_server_information (skeleton, + invocation, + "Phosh Notify Daemon", + "Phosh", + PHOSH_VERSION, + NOTIFICATIONS_SPEC_VERSION); + return TRUE; +} + + +static void +on_notification_expired (PhoshNotifyManager *self, + PhoshNotification *notification) +{ + guint id = 0; + + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + id = phosh_notification_get_id (notification); + + g_debug ("Notification %u expired", id); + + /* Transient notifications are closed rather than staying in the tray */ + if (phosh_notification_get_transient (notification)) { + phosh_notification_close (notification, + PHOSH_NOTIFICATION_REASON_EXPIRED); + } +} + + +static void +forget_unlock_notify (PhoshNotifyManager *self) +{ + if (!self->unlock_notify.notification) + return; + + g_clear_object (&self->unlock_notify.notification); + g_clear_pointer (&self->unlock_notify.action, g_free); +} + + +static void +invoke_action (PhoshNotifyManager *self, PhoshNotification *notification, const char *action) +{ + guint id; + + id = phosh_notification_get_id (notification); + + g_return_if_fail (id); + g_debug ("Emitting ActionInvoked: %d, %s", id, action); + + phosh_notification_do_action (notification, id, action); + + /* Resident notifications stay after being actioned */ + if (!phosh_notification_get_resident (notification)) { + phosh_notification_close (notification, + PHOSH_NOTIFICATION_REASON_DISMISSED); + } + g_signal_emit (self, signals[NOTIFICATION_ACTIVATED], 0, notification); +} + + + +static void +on_shell_lock_changed (PhoshNotifyManager* self, GParamSpec *pspec, PhoshShell *shell) +{ + gboolean locked; + + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + + locked = phosh_shell_get_locked (shell); + if (!locked && self->unlock_notify.notification) { + invoke_action (self, self->unlock_notify.notification, self->unlock_notify.action); + forget_unlock_notify (self); + } +} + + +static void +on_notification_actioned (PhoshNotifyManager *self, + const char *action, + PhoshNotification *notification) +{ + PhoshShell *shell = phosh_shell_get_default(); + + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + /* + * Postpone action when shell is locked. + * Only do this for the default action atm as other actions on the lock screen + * must be a result of action filters and hence don't need unlock + * TODO: Ensure this is really a result of a filtered action + * Eventually we want to allow other actions that need unlock: + * https://gitlab.freedesktop.org/xdg/xdg-specs/-/issues/103 + */ + if (phosh_shell_get_locked (shell) && g_strcmp0 (action, "default") == 0) { + PhoshLockscreenManager *lm = phosh_shell_get_lockscreen_manager (shell); + + /* Forget any pending actions */ + forget_unlock_notify (self); + + self->unlock_notify.notification = g_object_ref (notification); + self->unlock_notify.action = g_strdup (action); + + /* Scroll to unlock page */ + phosh_lockscreen_manager_set_page (lm, PHOSH_LOCKSCREEN_PAGE_UNLOCK); + } else { + invoke_action (self, notification, action); + } +} + + +static void +on_notification_closed (PhoshNotifyManager *self, + PhoshNotificationReason reason, + PhoshNotification *notification) +{ + guint id; + + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + + id = phosh_notification_get_id (notification); + + g_debug ("Emitting NotificationClosed: %d, %d", id, reason); + + phosh_dbus_notifications_emit_notification_closed (PHOSH_DBUS_NOTIFICATIONS (self), id, reason); +} + + +static gboolean +phosh_notify_manager_is_notification_enabled (PhoshNotification *notification) +{ + g_autofree char *munged_id = NULL; + g_autofree char *path = NULL; + g_autoptr (GSettings) settings = NULL; + GAppInfo *info; + const char *id; + + info = phosh_notification_get_app_info (notification); + if (!info) + return TRUE; + + id = g_app_info_get_id (info); + if (!info) + return TRUE; + + if (gm_str_is_null_or_empty (id)) + return TRUE; + + munged_id = phosh_munge_app_id (id); + path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL); + settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path); + + return g_settings_get_boolean (settings, NOTIFICATIONS_APP_KEY_ENABLE); +} + + +static GIcon * +parse_icon_data (GVariant *variant) +{ + GVariant *wrapped_data = NULL; + guchar *data = NULL; + GIcon *icon = NULL; + int width = 0; + int height = 0; + int row_stride = 0; + int has_alpha = 0; + int sample_size = 0; + int channels = 0; + + if (g_variant_is_of_type (variant, G_VARIANT_TYPE ("(iiibiiay)"))) { + gsize size_should_be; + + g_variant_get (variant, + "(iiibii@ay)", + &width, + &height, + &row_stride, + &has_alpha, + &sample_size, + &channels, + &wrapped_data); + + size_should_be = (height - 1) * row_stride + width * ((channels * sample_size + 7) / 8); + + if (size_should_be != g_variant_get_size (wrapped_data)) { + g_warning ("Rejecting image, %" G_GSIZE_FORMAT + " (expected) != %" G_GSIZE_FORMAT, + size_should_be, g_variant_get_size (wrapped_data)); + + return NULL; + } + + /* Extract a copy of the raw data */ + data = (guchar *) g_memdup2 (g_variant_get_data (wrapped_data), size_should_be); + + icon = G_ICON (gdk_pixbuf_new_from_data (data, + GDK_COLORSPACE_RGB, + has_alpha, + sample_size, + width, + height, + row_stride, + (GdkPixbufDestroyNotify) g_free, + NULL)); + } + + return icon; +} + + +static GIcon * +parse_icon_string (const char *string) +{ + g_autoptr (GFile) file = NULL; + GIcon *icon = NULL; + + if (string == NULL || strlen (string) < 1) { + return NULL; + } + + if (g_str_has_prefix (string, "file://")) { + file = g_file_new_for_uri (string); + icon = g_file_icon_new (file); + } else if (g_str_has_prefix (string, "/")) { + file = g_file_new_for_path (string); + icon = g_file_icon_new (file); + } else { + icon = g_themed_icon_new (string); + } + + return icon; +} + + +static void +phosh_notify_manager_add_application (PhoshNotifyManager *self, GAppInfo *info) +{ + g_autofree char *munged_id = NULL; + g_autofree char *path = NULL; + g_autoptr (GSettings) settings = NULL; + g_autoptr(GPtrArray) new_apps = NULL; + const char *id; + + id = g_app_info_get_id(info); + munged_id = phosh_munge_app_id (id); + if (g_strv_contains ((const char * const *)self->app_children, munged_id)) + return; + + g_debug ("Adding new application: %s/%s", id, munged_id); + new_apps = g_ptr_array_sized_new (g_strv_length (self->app_children) + 1); + for (int i = 0; i < g_strv_length (self->app_children); i++) + g_ptr_array_add (new_apps, self->app_children[i]); + + g_ptr_array_add (new_apps, munged_id); + g_ptr_array_add (new_apps, NULL); + + path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL); + settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path); + g_settings_set_string (settings, NOTIFICATIONS_APP_KEY_APP_ID, id); + g_settings_set_strv (self->settings, NOTIFICATIONS_KEY_APP_CHILDREN, + (const char * const *)new_apps->pdata); +} + + +static gboolean +handle_notify (PhoshDBusNotifications *skeleton, + GDBusMethodInvocation *invocation, + const char *app_name, + guint replaces_id, + const char *app_icon, + const char *summary, + const char *body, + const char *const *actions, + GVariant *hints, + int expire_timeout) +{ + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (skeleton); + PhoshNotification *notification = NULL; + GVariant *item; + GVariantIter iter; + guint id; + g_autofree char *desktop_id = NULL; + g_autofree char *source_id = NULL; + g_autofree char *escaped_body = NULL; + g_autoptr (GAppInfo) info = NULL; + PhoshNotificationUrgency urgency = PHOSH_NOTIFICATION_URGENCY_NORMAL; + g_autoptr (GIcon) data_gicon = NULL; + g_autoptr (GIcon) path_gicon = NULL; + g_autoptr (GIcon) app_gicon = NULL; + g_autoptr (GIcon) old_data_gicon = NULL; + gboolean transient = FALSE; + gboolean resident = FALSE; + g_autofree char *category = NULL; + g_autofree char *profile = NULL; + g_autofree char *sound_file = NULL; + GIcon *icon = NULL; + GIcon *image = NULL; + + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + + g_debug ("DBus call Notify: %s (%u): %s (%s), %s, %d", app_name, replaces_id, summary, body, app_icon, expire_timeout); + + app_gicon = parse_icon_string (app_icon); + + g_variant_iter_init (&iter, hints); + while ((item = g_variant_iter_next_value (&iter))) { + g_autofree char *key = NULL; + g_autoptr(GVariant) value = NULL; + + g_variant_get (item, "{sv}", &key, &value); + + if (g_strcmp0 (key, "urgency") == 0) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BYTE) && + (g_variant_get_byte(value) == PHOSH_NOTIFICATION_URGENCY_CRITICAL)) { + expire_timeout = 0; + urgency = PHOSH_NOTIFICATION_URGENCY_CRITICAL; + } + } else if ((g_strcmp0 (key, "image-data") == 0) || + (g_strcmp0 (key, "image_data") == 0)) { + data_gicon = parse_icon_data (value); + } else if ((g_strcmp0 (key, "image-path") == 0) || + (g_strcmp0 (key, "image_path") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) { + path_gicon = parse_icon_string (g_variant_get_string (value, NULL)); + } + } else if (g_strcmp0 (key, "icon_data") == 0) { + old_data_gicon = parse_icon_data (value); + } else if ((g_strcmp0 (key, "desktop_entry") == 0) || + (g_strcmp0 (key, "desktop-entry") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) + desktop_id = g_variant_dup_string (value, NULL); + } else if ((g_strcmp0 (key, "transient") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) + transient = g_variant_get_boolean (value); + else if (g_variant_is_of_type (value, G_VARIANT_TYPE_INT32)) + transient = !!g_variant_get_int32 (value); + } else if ((g_strcmp0 (key, "resident") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) + resident = g_variant_get_boolean (value); + } else if ((g_strcmp0 (key, "category") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) + category = g_variant_dup_string (value, NULL); + } else if ((g_strcmp0 (key, "x-phosh-fb-profile") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) + profile = g_variant_dup_string (value, NULL); + } else if ((g_strcmp0 (key, "sound-file") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_STRING)) + sound_file = g_variant_dup_string (value, NULL); + } else if ((g_strcmp0 (key, "suppress-sound") == 0)) { + if (g_variant_is_of_type (value, G_VARIANT_TYPE_BOOLEAN)) + profile = g_strdup ("quiet"); + } + + g_variant_unref (item); + } + + if (data_gicon) { + image = data_gicon; + } else if (path_gicon) { + image = path_gicon; + } else if (old_data_gicon) { + image = old_data_gicon; + } else { + image = NULL; + } + + icon = app_gicon; + + if (desktop_id) { + GDesktopAppInfo *desktop_info; + source_id = g_strdup_printf ("%s.desktop", desktop_id); + + desktop_info = phosh_get_desktop_app_info_for_app_id (desktop_id); + if (desktop_info) + info = G_APP_INFO (desktop_info); + } else if (app_name && g_strcmp0 (app_name, "notify-send")) { + /* When the app name is set (and isn't notify-send) use that + as it's better than nothing */ + source_id = g_strdup_printf ("legacy-app-%s", app_name); + } else { + /* Worse case: The notification gets it's own group, we don't know + where it came from */ + source_id = g_strdup_printf ("unknown-app-%i", self->unknown_source++); + } + + if (replaces_id) + notification = phosh_notification_list_get_by_id (self->list, replaces_id); + + escaped_body = phosh_util_escape_markup (body, TRUE); + + if (notification) { + id = replaces_id; + + g_object_set (notification, + "app_name", app_name, + "summary", summary, + "body", escaped_body, + "app-icon", icon, + "app-info", info, + "image", image, + "urgency", urgency, + "actions", actions, + "timestamp", NULL, + "sound-file", sound_file, + NULL); + } else { + g_autoptr (PhoshDBusNotification) dbus_notification = NULL; + + if (info) + phosh_notify_manager_add_application (self, info); + + id = phosh_notify_manager_get_notification_id (self); + dbus_notification = phosh_dbus_notification_new (id, + app_name, + info, + summary, + escaped_body, + icon, + image, + urgency, + (GStrv) actions, + transient, + resident, + category, + profile, + NULL); + phosh_notification_set_sound_file (PHOSH_NOTIFICATION (dbus_notification), sound_file); + + phosh_notify_manager_add_notification (self, + source_id, + expire_timeout, + PHOSH_NOTIFICATION (dbus_notification)); + } + + phosh_dbus_notifications_complete_notify (skeleton, invocation, id); + + return TRUE; +} + + +static void +phosh_notify_manager_notify_iface_init (PhoshDBusNotificationsIface *iface) +{ + iface->handle_close_notification = handle_close_notification; + iface->handle_get_capabilities = handle_get_capabilities; + iface->handle_get_server_information = handle_get_server_information; + iface->handle_notify = handle_notify; +} + + +static void +on_notifications_setting_changed (PhoshNotifyManager *self, + const char *key, + GSettings *settings) +{ + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (G_IS_SETTINGS (settings)); + + self->show_banners = g_settings_get_boolean (settings, PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS); +} + + +static void +on_notification_apps_setting_changed (PhoshNotifyManager *self, + const char *key, + GSettings *settings) +{ + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (G_IS_SETTINGS (settings)); + + g_strfreev (self->app_children); + self->app_children = g_settings_get_strv (settings, NOTIFICATIONS_KEY_APP_CHILDREN); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (user_data); + + g_debug ("Acquired name %s", name); + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshNotifyManager *self = user_data; + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/freedesktop/Notifications", + NULL); +} + + +static void +phosh_notify_manager_dispose (GObject *object) +{ + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_object (&self->settings); + g_clear_object (&self->feedback); + g_clear_object (&self->list); + + G_OBJECT_CLASS (phosh_notify_manager_parent_class)->dispose (object); +} + + +static void +phosh_notify_manager_finalize (GObject *object) +{ + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object); + + g_strfreev (self->app_children); + + G_OBJECT_CLASS (phosh_notify_manager_parent_class)->finalize (object); +} + + + +static void +phosh_notify_manager_constructed (GObject *object) +{ + PhoshNotifyManager *self = PHOSH_NOTIFY_MANAGER (object); + PhoshShell *shell = phosh_shell_get_default (); + + G_OBJECT_CLASS (phosh_notify_manager_parent_class)->constructed (object); + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + NOTIFY_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); + + self->settings = g_settings_new (PHOSH_NOTIFICATIONS_SCHEMA_ID); + g_signal_connect_swapped (self->settings, "changed::" PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS, + G_CALLBACK (on_notifications_setting_changed), self); + on_notifications_setting_changed (self, NULL, self->settings); + + g_signal_connect_swapped (self->settings, "changed::" NOTIFICATIONS_KEY_APP_CHILDREN, + G_CALLBACK (on_notification_apps_setting_changed), self); + on_notification_apps_setting_changed (self, NULL, self->settings); + + g_signal_connect_swapped (shell, "notify::locked", G_CALLBACK (on_shell_lock_changed), self); + + self->feedback = phosh_notify_feedback_new (self->list); +} + + +static void +phosh_notify_manager_class_init (PhoshNotifyManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_notify_manager_constructed; + object_class->dispose = phosh_notify_manager_dispose; + object_class->finalize = phosh_notify_manager_finalize; + + /** + * PhoshNotifyManager::new-notification: + * @self: the #PhoshNotifyManager + * @notification: the new #PhoshNotification + * + * Emitted when a new notification is received and a banner should (possibly) + * be shown + */ + signals[NEW_NOTIFICATION] = g_signal_new ("new-notification", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + PHOSH_TYPE_NOTIFICATION); + + /** + * PhoshNotifyManager::notification-activated: + * @self: the #PhoshNotifyManager + * @notification: the #PhoshNotification + * + * Emitted when the action on a notification gets activated. + */ + signals[NOTIFICATION_ACTIVATED] = g_signal_new ("notification-activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, + 1, + PHOSH_TYPE_NOTIFICATION); +} + + +static void +phosh_notify_manager_init (PhoshNotifyManager *self) +{ + self->next_id = 1; + + self->list = phosh_notification_list_new (); +} + +/** + * phosh_notify_manager_get_default: + * + * Get the notify manager singleton + * + * Returns:(transfer none): The notify manager singleton + */ +PhoshNotifyManager * +phosh_notify_manager_get_default (void) +{ + static PhoshNotifyManager *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_NOTIFY_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + + return instance; +} + +/** + * phosh_notify_manager_get_list: + * @self: the #PhoshNotifyManager + * + * Get the #PhoshNotificationList of current notifications + * + * Returns:(transfer none): The #PhoshNotificationList + */ +PhoshNotificationList * +phosh_notify_manager_get_list (PhoshNotifyManager *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), NULL); + + return self->list; +} + +/** + * phosh_notify_manager_get_show_banners: + * @self: the #PhoshNotifyManager + * + * Are notififcation banners enabled + * + * Returns: %TRUE if banners should be shown, otherwise %FALSE + */ +gboolean +phosh_notify_manager_get_show_banners (PhoshNotifyManager *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + + return self->show_banners; +} + +/** + * phosh_notify_manager_get_notification_id: + * @self: the #PhoshNotifyManager + * + * Get a notification id + * + * Returns: a notification id that can be used to create new + * notifications. + */ +guint +phosh_notify_manager_get_notification_id (PhoshNotifyManager *self) +{ + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + + return self->next_id++; +} + +/** + * phosh_notify_manager_add_notification + * @self: the #PhoshNotifyManager + * @source_id: The notification source's app_id + * @expire_timeout: When the notification should expire + * @notification: The notification + * + * Adds @notification to the current list of notifications. This is the single + * entry point to submit notifications. + */ +void +phosh_notify_manager_add_notification (PhoshNotifyManager *self, + const char *source_id, + int expire_timeout, + PhoshNotification *notification) +{ + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + g_return_if_fail (source_id); + + if (!phosh_notify_manager_is_notification_enabled (notification)) + return; + + if (expire_timeout == -1) + expire_timeout = PHOSH_NOTIFICATION_DEFAULT_TIMEOUT; + + phosh_notification_list_add (self->list, source_id, notification); + + g_signal_connect_object (notification, + "expired", + G_CALLBACK (on_notification_expired), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (notification, + "actioned", + G_CALLBACK (on_notification_actioned), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (notification, + "closed", + G_CALLBACK (on_notification_closed), + self, + G_CONNECT_SWAPPED); + + if (expire_timeout) + phosh_notification_expires (notification, expire_timeout); + + g_signal_emit (self, signals[NEW_NOTIFICATION], 0, notification); +} + + +gboolean +phosh_notify_manager_close_notification_by_id (PhoshNotifyManager *self, + int id, + PhoshNotificationReason reason) +{ + PhoshNotification *notification = NULL; + + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + + notification = phosh_notification_list_get_by_id (self->list, id); + if (!notification) + return FALSE; + + phosh_notification_close (notification, reason); + return TRUE; +} + +/** + * phosh_notify_manager_get_show_notfication_banner: + * @self: the #PhoshNotifyManager + * @notification: the #PhoshNotification in question + * + * Checks whether a #PhoshNotificationBanner should be displayed + * for the given #PhoshNotification according to current policy. + * + * Returns: %TRUE if the banner should be shown, otherwise %FALSE + */ +gboolean +phosh_notify_manager_get_show_notification_banner (PhoshNotifyManager *self, + PhoshNotification *notification) +{ + g_autoptr (GSettings) settings = NULL; + g_autofree char *path = NULL; + g_autofree char *munged_id = NULL; + GAppInfo *app_info; + gboolean show; + + g_return_val_if_fail (PHOSH_IS_NOTIFY_MANAGER (self), FALSE); + + if (!self->show_banners) + return FALSE; + + if (phosh_notification_get_urgency (notification) == PHOSH_NOTIFICATION_URGENCY_CRITICAL) + return TRUE; + + app_info = phosh_notification_get_app_info (notification); + if (!app_info) + return TRUE; + + munged_id = phosh_munge_app_id (g_app_info_get_id(app_info)); + path = g_strconcat (NOTIFICATIONS_APP_PREFIX, "/", munged_id, "/", NULL); + settings = g_settings_new_with_path (NOTIFICATIONS_APP_SCHEMA_ID, path); + show = g_settings_get_boolean (settings, NOTIFICATIONS_APP_KEY_SHOW_BANNERS); + + g_debug ("Show banners for %s: %d", munged_id, show); + return show; +} + +/** + * phosh_notify_manager_close_all: + * @self: the #PhoshNotifyManager + * @rease: the #PhoshNotificationReason + * + * Closes all notifications using #PhoshNotificationReason as reason. + */ +void +phosh_notify_manager_close_all_notifications (PhoshNotifyManager *self, + PhoshNotificationReason reason) +{ + GListModel *source_list; + + source_list = G_LIST_MODEL (phosh_notify_manager_get_list (self)); + + for (int i = g_list_model_get_n_items(G_LIST_MODEL (source_list)); i > 0; i--) { + g_autoptr (GListModel) notif_list = G_LIST_MODEL (g_list_model_get_object (source_list, i-1)); + + for (int j = g_list_model_get_n_items(G_LIST_MODEL (notif_list)); j > 0; j--) { + g_autoptr (PhoshNotification) notification = PHOSH_NOTIFICATION (g_list_model_get_object (notif_list, j-1)); + phosh_notification_close (notification, PHOSH_NOTIFICATION_REASON_DISMISSED); + } + } +} + +/** + * phosh_notify_manager_add_shell_notification: + * @self: The #PhoshNotifyManager + * @notification: The notification to add + * @id: The id + * @expire_timeout: The expiration timeout + * + * Adds a notification to the list of notifications. If `id` is not `0` + * an existing notification is replaced. + * + * If the notification has no `app-name` or `app-icon` set then a shell + * default is filled in. + * + * If the expire_timeout is greater than `0` the notification is + * marked as transient. + * + * The returned `id` can be used at a later point to retract or + * replace the notification. + * + * Returns: The id of the notification + */ +guint +phosh_notify_manager_add_shell_notification (PhoshNotifyManager *self, + PhoshNotification *notification, + guint id, + int expire_timeout) +{ + g_autoptr (GDesktopAppInfo) info = NULL; + + info = g_desktop_app_info_new (PHOSH_APP_ID ".desktop"); + g_return_val_if_fail (G_IS_DESKTOP_APP_INFO (info), 0); + + if (!id) { + id = phosh_notify_manager_get_notification_id (self); + phosh_notification_set_id (notification, id); + } + + /* We don't just set the app_info but name and icon separately to + * not overwrite values set by the caller */ + if (g_strcmp0 (phosh_notification_get_app_name (notification), _("Notification")) == 0) { + const char *name = g_app_info_get_name (G_APP_INFO (info)); + + phosh_notification_set_app_name (notification, name); + } + + if (!phosh_notification_get_app_icon (notification)) { + GIcon *icon = g_app_info_get_icon (G_APP_INFO (info)); + + phosh_notification_set_app_icon (notification, icon); + } + + phosh_notification_set_transient (notification, !!expire_timeout); + + phosh_notify_manager_add_notification (self, PHOSH_APP_ID ".desktop", + expire_timeout, + PHOSH_NOTIFICATION (notification)); + return id; +} diff --git a/src/notifications/notify-manager.h b/src/notifications/notify-manager.h new file mode 100644 index 000000000..11757e24a --- /dev/null +++ b/src/notifications/notify-manager.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include "notify-manager.h" +#include "notification-list.h" +#include "dbus/notify-dbus.h" +#include + +G_BEGIN_DECLS + +#define PHOSH_NOTIFICATIONS_SCHEMA_ID "org.gnome.desktop.notifications" +#define PHOSH_NOTIFICATIONS_KEY_SHOW_BANNERS "show-banners" + +#define PHOSH_TYPE_NOTIFY_MANAGER (phosh_notify_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshNotifyManager, phosh_notify_manager, PHOSH, NOTIFY_MANAGER, + PhoshDBusNotificationsSkeleton) + +PhoshNotifyManager *phosh_notify_manager_get_default (void); +PhoshNotificationList *phosh_notify_manager_get_list (PhoshNotifyManager *self); +gboolean phosh_notify_manager_get_show_banners (PhoshNotifyManager *self); +guint phosh_notify_manager_get_notification_id (PhoshNotifyManager *self); +void phosh_notify_manager_add_notification (PhoshNotifyManager *self, + const char *source_id, + int expire_timeout, + PhoshNotification *notification); +gboolean phosh_notify_manager_close_notification_by_id (PhoshNotifyManager *self, + int id, + PhoshNotificationReason reason); +void phosh_notify_manager_close_all_notifications (PhoshNotifyManager *self, + PhoshNotificationReason reaseon); +gboolean phosh_notify_manager_get_show_notification_banner (PhoshNotifyManager *self, + PhoshNotification *notification); + +guint phosh_notify_manager_add_shell_notification (PhoshNotifyManager *self, + PhoshNotification *notification, + guint id, + int expire_timeout); +G_END_DECLS diff --git a/src/notifications/timestamp-label-priv.h b/src/notifications/timestamp-label-priv.h new file mode 100644 index 000000000..ba6fcfa7a --- /dev/null +++ b/src/notifications/timestamp-label-priv.h @@ -0,0 +1,16 @@ +/* + * Copyright © 2020 Lugsole + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + + +G_BEGIN_DECLS + +char *phosh_time_diff_in_words (GDateTime *dt, GDateTime *dt_now); + +G_END_DECLS diff --git a/src/notifications/timestamp-label.c b/src/notifications/timestamp-label.c new file mode 100644 index 000000000..f340b78ed --- /dev/null +++ b/src/notifications/timestamp-label.c @@ -0,0 +1,394 @@ +/* + * Copyright © 2020 Lugsole + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#define G_LOG_DOMAIN "phosh-timestamp-label" + +#include "timestamp-label.h" +#include "timestamp-label-priv.h" +#include "phosh-config.h" +#include + +/** + * PhoshTimestampLabel: + * + * A simple way of displaying a time difference + * + * The #PhoshTimestampLabel is used to display the time difference between + * the timestamp stored in the #PhoshTimestampLabel and the current time. + */ + + +struct _PhoshTimestampLabel { + GtkBin parent; + + GtkLabel *label; + GDateTime *date; + guint refresh_time; +}; + + +enum { + PROP_0, + PROP_TIMESTAMP, + LAST_PROP +}; +static GParamSpec *props[LAST_PROP]; + + +G_DEFINE_TYPE (PhoshTimestampLabel, phosh_timestamp_label, GTK_TYPE_BIN) + + +#define SECONDS_PER_MINUTE 60.0 +#define SECONDS_PER_HOUR 3600.0 +#define SECONDS_PER_DAY 86400.0 +#define SECONDS_PER_MONTH 2592000.0 +#define SECONDS_PER_YEAR 31536000.0 +#define MINUTES_PER_DAY 1440.0 +#define MINUTES_PER_YEAR 525600.0 +#define MINUTES_PER_QUARTER 131400.0 + +/** + * phosh_time_diff_in_words: + * @dt: The target time + * @dt_now: the current time + * + * Generate a string to represent a #GDateTime difference. Currently @dt must be in the past of @dt_now. + * Times in the future aren't supported. + * + * Based on [ChattyListRow](https://source.puri.sm/Librem5/chatty/blob/master/src/chatty-list-row.c#L47) + * itself based on the ruby on rails method 'distance_of_time_in_words' + * + * Returns: (transfer full): the generated string + */ +char * +phosh_time_diff_in_words (GDateTime *dt, GDateTime *dt_now) +{ + char *result = NULL; + int seconds, minutes, hours, days, months, years, offset, remainder; + double dist_in_seconds; + + dist_in_seconds = g_date_time_difference (dt_now, dt) / G_TIME_SPAN_SECOND; + + seconds = (int) dist_in_seconds; + minutes = (int) (dist_in_seconds / SECONDS_PER_MINUTE); + hours = (int) (dist_in_seconds / SECONDS_PER_HOUR); + days = (int) (dist_in_seconds / SECONDS_PER_DAY); + months = (int) (dist_in_seconds / SECONDS_PER_MONTH); + years = (int) (dist_in_seconds / SECONDS_PER_YEAR); + + switch (minutes) { + case 0 ... 1: + switch (seconds) { + case 0 ... 14: + /* Translators: Point in time, use a short word or abbreviation */ + /* Please stick to a maximum of 12 chars */ + result = g_strdup (_("now")); + break; + case 15 ... 29: + /* Translators: abbreviated time difference "Less than 30 seconds" */ + /* Please stick to a maximum of 12 chars */ + result = g_strdup_printf (_("<30s")); + break; + case 30 ... 59: + /* Translators: abbreviated time difference "Less than one minute" */ + /* Please stick to a maximum of 12 chars */ + result = g_strdup_printf (_("<1m")); + break; + default: + /* Translators: abbreviated time difference "About one minute" */ + /* Please stick to a maximum of 12 chars */ + result = g_strdup_printf (_("~1m")); + break; + } + break; + + case 2 ... 44: + /* Translators: abbreviated, exact time difference "1 minute ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("%dm", "%dm", minutes), minutes); + break; + case 45 ... 89: + hours = 1; + G_GNUC_FALLTHROUGH; + case 90 ... 1439: + /* Translators: abbreviated time difference "About 3 hours ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("~%dh", "~%dh", hours), hours); + break; + case 1440 ... 2529: + /* Translators: abbreviated time difference "About 1 day ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (_("~1d")); + break; + case 2530 ... 43199: + /* Translators: abbreviated, exact time difference "3 days ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("%dd", "%dd", days), days); + break; + case 43200 ... 86399: + /* Translators: abbreviated time difference "About 1 month ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (_("~1mo")); + break; + case 86400 ... 525600: + /* Translators: abbreviated, exact time difference "3 months ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("%dmo", "%dmos", months), months); + break; + + default: + offset = ((float)years / 4.0) * MINUTES_PER_DAY; + remainder = (minutes - offset) % (int)MINUTES_PER_YEAR; + + if (remainder < MINUTES_PER_QUARTER) { + /* Translators: abbreviated time difference "About 5 years ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("~%dy", "~%dy", years), years); + } else if (remainder < (3 * MINUTES_PER_QUARTER)) { + /* Translators: abbreviated time difference "Over 5 years ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("Over %dy", "Over %dy", years), years); + } else { + ++years; + /* Translators: abbreviated time difference "almost 5 years ago" */ + /* Please stick to a maximum of 4 chars for the time unit */ + result = g_strdup_printf (ngettext ("Almost %dy", "Almost %dy", years), years); + } + break; + } + + return result; +} + +/** + * phosh_time_ago_in_words: + * @dt: the #GDateTime to represent + * + * Generate a string to represent a #GDateTime. Note that @dt must be in the past. + * + * Based on [ChattyListRow](https://source.puri.sm/Librem5/chatty/blob/master/src/chatty-list-row.c#L47) + * itself based on the ruby on rails method 'distance_of_time_in_words' + * + * Returns: (transfer full): the generated string + */ +static char * +phosh_time_ago_in_words (GDateTime *dt) +{ + g_autoptr (GDateTime) dt_now = g_date_time_new_now_local (); + + return phosh_time_diff_in_words (dt, dt_now); +} + +static GTimeSpan +phosh_timestamp_label_calc_timeout (PhoshTimestampLabel *self) +{ + + g_autoptr (GDateTime) time_now = g_date_time_new_now_local (); + g_autoptr (GDateTime) timeout_time = NULL; + int seconds, minutes, hours, days, months; + double dist_in_seconds; + GTimeSpan timeout_diff; + + dist_in_seconds = g_date_time_difference (time_now, self->date) / G_TIME_SPAN_SECOND; + seconds = (int) dist_in_seconds; + minutes = (int) (dist_in_seconds / SECONDS_PER_MINUTE); + hours = (int) (dist_in_seconds / SECONDS_PER_HOUR); + days = (int) (dist_in_seconds / SECONDS_PER_DAY); + months = (int) (dist_in_seconds / SECONDS_PER_MONTH); + + switch (minutes) { + case 0 ... 1: + + switch (seconds) { + case 0 ... 14: + timeout_time = g_date_time_add_seconds (self->date, 15); + break; + case 15 ... 29: + timeout_time = g_date_time_add_seconds (self->date, 30); + break; + case 30 ... 59: + timeout_time = g_date_time_add_minutes (self->date, 1); + break; + default: + timeout_time = g_date_time_add_minutes (self->date, 2); + break; + } + break; + + case 2 ... 44: + timeout_time = g_date_time_add_minutes (self->date, minutes + 1); + break; + case 45 ... 89: + timeout_time = g_date_time_add_minutes (self->date, 90); + break; + case 90 ... 1439: + timeout_time = g_date_time_add_hours (self->date, hours + 1); + break; + case 1440 ... 43199: + timeout_time = g_date_time_add_days (self->date, days + 1); + break; + case 43200 ... 525600: + timeout_time = g_date_time_add_months (self->date, months + 1); + break; + default: + timeout_time = g_date_time_add_months (self->date, months + 1); + break; + } + timeout_diff = g_date_time_difference (timeout_time, time_now); + g_debug ("time out duration: %" G_GINT64_FORMAT, timeout_diff); + return timeout_diff; +} + + +static gboolean +phosh_timestamp_label_update (PhoshTimestampLabel *self) +{ + g_autofree char *str = NULL; + + if (self->date != NULL) { + GTimeSpan time; + + str = phosh_time_ago_in_words (self->date); + gtk_label_set_label (self->label, str); + + g_clear_handle_id (&(self->refresh_time), g_source_remove); + time = phosh_timestamp_label_calc_timeout (self); + self->refresh_time = g_timeout_add (time / G_TIME_SPAN_MILLISECOND, + (GSourceFunc) phosh_timestamp_label_update, + self); + g_source_set_name_by_id (self->refresh_time, "[PhoshTimestampLable] refresh"); + } else { + gtk_label_set_label (self->label, ""); + + g_clear_handle_id (&(self->refresh_time), g_source_remove); + } + return G_SOURCE_REMOVE; +} + + +static void +phosh_timestamp_label_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTimestampLabel *self = PHOSH_TIMESTAMP_LABEL (object); + + switch (property_id) { + case PROP_TIMESTAMP: + g_value_set_boxed (value, phosh_timestamp_label_get_timestamp (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_timestamp_label_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshTimestampLabel *self = PHOSH_TIMESTAMP_LABEL (object); + + switch (property_id) { + case PROP_TIMESTAMP: + phosh_timestamp_label_set_timestamp (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_timestamp_label_dispose (GObject *object) +{ + PhoshTimestampLabel *self = PHOSH_TIMESTAMP_LABEL (object); + + g_clear_pointer (&self->date, g_date_time_unref); + g_clear_handle_id (&(self->refresh_time), g_source_remove); + + G_OBJECT_CLASS (phosh_timestamp_label_parent_class)->dispose (object); +} + + +static void +phosh_timestamp_label_class_init (PhoshTimestampLabelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_timestamp_label_dispose; + object_class->set_property = phosh_timestamp_label_set_property; + object_class->get_property = phosh_timestamp_label_get_property; + + props[PROP_TIMESTAMP] = + g_param_spec_boxed ( + "timestamp", + "Timestamp", + "The label's timestamp", + G_TYPE_DATE_TIME, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/timestamp-label.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshTimestampLabel, label); +} + + +static void +phosh_timestamp_label_init (PhoshTimestampLabel *self) +{ + PangoAttrList *attrs = pango_attr_list_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); + + pango_attr_list_insert (attrs, pango_attr_font_features_new ("tnum=1")); + gtk_label_set_attributes (self->label, attrs); + + g_clear_pointer (&attrs, pango_attr_list_unref); +} + + +PhoshTimestampLabel * +phosh_timestamp_label_new (void) +{ + return g_object_new (PHOSH_TYPE_TIMESTAMP_LABEL, NULL); +} + + +GDateTime * +phosh_timestamp_label_get_timestamp (PhoshTimestampLabel *self) +{ + g_return_val_if_fail (PHOSH_IS_TIMESTAMP_LABEL (self), NULL); + return self->date; +} + + +void +phosh_timestamp_label_set_timestamp (PhoshTimestampLabel *self, + GDateTime *date) +{ + g_return_if_fail (PHOSH_IS_TIMESTAMP_LABEL (self)); + + g_debug ("notification setting timestamp %d %d", self->date == NULL, date == NULL); + + if (self->date && date && g_date_time_compare (self->date, date) == 0) + return; + + g_clear_pointer (&self->date, g_date_time_unref); + if (date != NULL) + self->date = g_date_time_ref (date); + phosh_timestamp_label_update (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TIMESTAMP]); +} diff --git a/src/notifications/timestamp-label.h b/src/notifications/timestamp-label.h new file mode 100644 index 000000000..7010e1b19 --- /dev/null +++ b/src/notifications/timestamp-label.h @@ -0,0 +1,23 @@ +/* + * Copyright © 2020 Lugsole + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TIMESTAMP_LABEL (phosh_timestamp_label_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTimestampLabel, phosh_timestamp_label, PHOSH, TIMESTAMP_LABEL, GtkBin) + +PhoshTimestampLabel *phosh_timestamp_label_new (void); +void phosh_timestamp_label_set_timestamp (PhoshTimestampLabel *self, + GDateTime *date); +GDateTime * phosh_timestamp_label_get_timestamp (PhoshTimestampLabel *self); + +G_END_DECLS diff --git a/src/osd-window.c b/src/osd-window.c new file mode 100644 index 000000000..d0e58c447 --- /dev/null +++ b/src/osd-window.c @@ -0,0 +1,296 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-osd-window" + +#include "phosh-config.h" +#include "util.h" + +#include "osd-window.h" +#include "layersurface-priv.h" + +#include + +/** + * PhoshOsdWindow: + * + * A OSD Window + * + * The #PhoshOsdWindow displays contents fed via the + * OSD (on screen display) DBus interface. + */ + +enum { + PROP_0, + PROP_CONNECTOR, + PROP_LABEL, + PROP_ICON_NAME, + PROP_LEVEL, + PROP_MAX_LEVEL, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshOsdWindow { + PhoshSystemModal parent; + + char *connector; + char *label; + char *icon_name; + gdouble level; + gdouble max_level; + + GtkWidget *lbl; + GtkWidget *icon; + GtkWidget *bar; + GtkWidget *box; + GtkGesture *click_gesture; +} PhoshOsdWindow; + + +G_DEFINE_TYPE (PhoshOsdWindow, phosh_osd_window, PHOSH_TYPE_SYSTEM_MODAL) + + +static void +adjust_icon (PhoshOsdWindow *self, gboolean box_visible) +{ + int size; + + gtk_widget_set_visible (self->box, box_visible); + + size = box_visible ? 16 : 32; + gtk_image_set_pixel_size (GTK_IMAGE (self->icon), size); +} + + +static void +set_label (PhoshOsdWindow *self, char *label) +{ + gboolean visible; + + g_free (self->label); + self->label = label; + gtk_label_set_label (GTK_LABEL (self->lbl), self->label); + + visible = !gm_str_is_null_or_empty (label); + gtk_widget_set_visible (GTK_WIDGET (self->lbl), visible); + adjust_icon (self, visible); +} + + +static void +set_level (PhoshOsdWindow *self, double level) +{ + gboolean visible; + + self->level = level; + + if (level >= 0.0) + gtk_level_bar_set_value (GTK_LEVEL_BAR (self->bar), level); + + visible = level >= 0.0; + gtk_widget_set_visible (self->bar, visible); + adjust_icon (self, visible); +} + + + +static void +phosh_osd_window_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshOsdWindow *self = PHOSH_OSD_WINDOW (obj); + + switch (prop_id) { + case PROP_CONNECTOR: + g_free (self->connector); + self->connector = g_value_dup_string (value); + break; + case PROP_LABEL: + set_label (self, g_value_dup_string (value)); + break; + case PROP_ICON_NAME: + g_free (self->icon_name); + self->icon_name = g_value_dup_string (value); + gtk_image_set_from_icon_name (GTK_IMAGE (self->icon), self->icon_name, GTK_ICON_SIZE_INVALID); + break; + case PROP_LEVEL: + set_level (self, g_value_get_double (value)); + break; + case PROP_MAX_LEVEL: + self->max_level = g_value_get_double (value); + gtk_level_bar_set_max_value (GTK_LEVEL_BAR (self->bar), self->max_level); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_osd_window_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshOsdWindow *self = PHOSH_OSD_WINDOW (obj); + + switch (prop_id) { + case PROP_CONNECTOR: + g_value_set_string (value, self->connector ? self->connector : ""); + break; + case PROP_LABEL: + g_value_set_string (value, self->label ? self->label : ""); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name ? self->icon_name : ""); + break; + case PROP_LEVEL: + g_value_set_double (value, self->level); + break; + case PROP_MAX_LEVEL: + g_value_set_double (value, self->max_level); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +on_button_released (PhoshOsdWindow *self) +{ + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +static void +phosh_osd_window_map (GtkWidget *widget) +{ + PhoshOsdWindow *self = PHOSH_OSD_WINDOW (widget); + int width; + + GTK_WIDGET_CLASS (phosh_osd_window_parent_class)->map (widget); + + width = gtk_widget_get_allocated_width (widget); + phosh_layer_surface_set_size (PHOSH_LAYER_SURFACE (self), width, -1); +} + + +static void +phosh_osd_window_finalize (GObject *obj) +{ + PhoshOsdWindow *self = PHOSH_OSD_WINDOW (obj); + + g_free (self->connector); + g_free (self->label); + g_free (self->icon_name); + + G_OBJECT_CLASS (phosh_osd_window_parent_class)->finalize (obj); +} + + +static void +phosh_osd_window_class_init (PhoshOsdWindowClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_osd_window_get_property; + object_class->set_property = phosh_osd_window_set_property; + object_class->finalize = phosh_osd_window_finalize; + + widget_class->map = phosh_osd_window_map; + + /* TODO: currently unused */ + props[PROP_CONNECTOR] = + g_param_spec_string ("connector", + "Connector", + "Connector to use for osd display", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_LABEL] = + g_param_spec_string ("label", + "Label", + "Label to show on osd", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", + "Icon Name", + "Name of icon to use on osd", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_LEVEL] = + g_param_spec_double ("level", + "Level", + "Level of bar to display on osd", + -1.0, + G_MAXDOUBLE, + 0.0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_MAX_LEVEL] = + g_param_spec_double ("max-level", + "Maximum Level", + "Maximum level of bar to display on osd", + 0.0, + G_MAXDOUBLE, + 0.0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/osd-window.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshOsdWindow, lbl); + gtk_widget_class_bind_template_child (widget_class, PhoshOsdWindow, icon); + gtk_widget_class_bind_template_child (widget_class, PhoshOsdWindow, bar); + gtk_widget_class_bind_template_child (widget_class, PhoshOsdWindow, box); + gtk_widget_class_bind_template_child (widget_class, PhoshOsdWindow, click_gesture); + gtk_widget_class_bind_template_callback (widget_class, on_button_released); + + gtk_widget_class_set_css_name (widget_class, "phosh-osd-window"); +} + + +static void +phosh_osd_window_init (PhoshOsdWindow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + gtk_widget_add_events (GTK_WIDGET (self), GDK_BUTTON_RELEASE_MASK); +} + + +GtkWidget * +phosh_osd_window_new (const char *connector, + const char *label, + const char *icon_name, + double level, + double max_level) +{ + return g_object_new (PHOSH_TYPE_OSD_WINDOW, + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP, + "width", 16, + "connector", connector, + "label",label, + "icon-name", icon_name, + "valign", GTK_ALIGN_CENTER, + "level", level, + "max-level", max_level, + "kbd-interactivity", FALSE, + NULL); +} diff --git a/src/osd-window.h b/src/osd-window.h new file mode 100644 index 000000000..0b91f205d --- /dev/null +++ b/src/osd-window.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "system-modal.h" + +#define PHOSH_TYPE_OSD_WINDOW (phosh_osd_window_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshOsdWindow, phosh_osd_window, PHOSH, OSD_WINDOW, PhoshSystemModal) + +GtkWidget *phosh_osd_window_new (const char *connector, + const char *label, + const char *icon_name, + double level, + double max_level); diff --git a/src/osk-manager.c b/src/osk-manager.c new file mode 100644 index 000000000..f77f6ebb5 --- /dev/null +++ b/src/osk-manager.c @@ -0,0 +1,322 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-osk-manager" + +#include "lockscreen-manager.h" +#include "osk-manager.h" +#include "phosh-osk0-dbus.h" +#include "shell-priv.h" + +#include + +#define VIRTBOARD_DBUS_NAME "sm.puri.OSK0" +#define VIRTBOARD_DBUS_OBJECT "/sm/puri/OSK0" + +/** + * PhoshOskManager: + * + * A manager that handles the OSK + * + * The #PhoshOskManager is responsible for handling the on screen keyboard. + * It tracks the OSKs visible property and can toggle the state. Note that + * there's no way to ensure keyboard state via this interface as it just + * uses DBus to express preference. Any text input can make the keyboard + * show again. + */ +enum { + PROP_0, + PROP_AVAILABLE, + PROP_VISIBLE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshOskManager +{ + GObject parent; + + /* Currently the only impl. We can use an interface once we support + * different OSK types */ + PhoshDBusOSK0 *proxy; + GSettings *a11y_settings; + + gboolean visible; + gboolean has_name_owner; + gboolean enabled; +}; +G_DEFINE_TYPE (PhoshOskManager, phosh_osk_manager, G_TYPE_OBJECT) + + +static void +phosh_osk_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshOskManager *self = PHOSH_OSK_MANAGER (object); + + switch (property_id) { + case PROP_VISIBLE: + g_value_set_boolean (value, self->visible); + break; + case PROP_AVAILABLE: + g_value_set_boolean (value, phosh_osk_manager_get_available (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_osk0_set_visible_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusOSK0 *proxy = PHOSH_DBUS_OSK0 (source_object); + PhoshOskManager *self = PHOSH_OSK_MANAGER (user_data); + g_autoptr (GError) err = NULL; + gboolean visible; + + if (!phosh_dbus_osk0_call_set_visible_finish (proxy, res, &err)) + g_warning ("Unable to toggle OSK: %s", err->message); + + visible = phosh_dbus_osk0_get_visible (proxy); + if (visible != self->visible) { + self->visible = visible; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE]); + } + + g_object_unref (self); +} + + +static void +set_visible_real (PhoshOskManager *self, gboolean visible) +{ + g_return_if_fail (G_IS_DBUS_PROXY (self->proxy)); + + g_debug ("Setting osk to %svisible", visible ? "" : "not "); + phosh_dbus_osk0_call_set_visible ( + self->proxy, + visible, + NULL, + on_osk0_set_visible_done, + g_object_ref (self)); +} + + +static void +dbus_name_owner_changed_cb (PhoshOskManager *self, gpointer data) +{ + g_autofree char *name_owner = NULL; + + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + + name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->proxy)); + g_debug ("OSK bus '%s' owned by %s", VIRTBOARD_DBUS_NAME, name_owner ? name_owner : "nobody"); + + self->has_name_owner = name_owner ? TRUE : FALSE; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AVAILABLE]); +} + + +static void +on_screen_keyboard_enabled_changed (PhoshOskManager *self) +{ + gboolean enabled; + + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + + enabled = g_settings_get_boolean(self->a11y_settings, "screen-keyboard-enabled"); + if (enabled == self->enabled) + return; + + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AVAILABLE]); +} + + +static void +on_availability_changed (PhoshOskManager *self, GParamSpec *pspec, gpointer unused) +{ + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + + /* Sync on visibility when osk is unavailable so buttons, etc look correct */ + if (!phosh_osk_manager_get_available (self)) + phosh_osk_manager_set_visible (self, FALSE); +} + + +static void +on_visible_changed (PhoshOskManager *self, GParamSpec *pspec, PhoshDBusOSK0 *proxy) +{ + gboolean visible; + + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + g_return_if_fail (G_IS_DBUS_PROXY (proxy)); + + visible = phosh_dbus_osk0_get_visible (proxy); + /* Just need to sync the property, osk shows/hides itself */ + if (visible != self->visible) { + self->visible = visible; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VISIBLE]); + } +} + + +static void +on_shell_locked_changed (PhoshOskManager *self, GParamSpec *pspec, gpointer unused) +{ + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + + /* Hide OSK on lock screen lock */ + if (phosh_shell_get_locked (phosh_shell_get_default ())) + set_visible_real (self, FALSE); +} + + +static void +phosh_osk_manager_constructed (GObject *object) +{ + PhoshOskManager *self = PHOSH_OSK_MANAGER (object); + PhoshShell *shell; + g_autoptr (GError) err = NULL; + + G_OBJECT_CLASS (phosh_osk_manager_parent_class)->constructed (object); + + self->proxy = phosh_dbus_osk0_proxy_new_for_bus_sync ( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION, + VIRTBOARD_DBUS_NAME, + VIRTBOARD_DBUS_OBJECT, + NULL, + &err); + + if (self->proxy == NULL) { + g_warning ("Failed to register with osk: %s", err->message); + g_return_if_fail (self->proxy); + } + + g_signal_connect_swapped ( + self->proxy, + "notify::g-name-owner", + G_CALLBACK (dbus_name_owner_changed_cb), + self); + dbus_name_owner_changed_cb (self, NULL); + + g_signal_connect (self, + "notify::available", + G_CALLBACK (on_availability_changed), + NULL); + /* Don't use a binding to keep visibility prop r/o */ + g_signal_connect_swapped (self->proxy, + "notify::visible", + G_CALLBACK (on_visible_changed), + self); + on_visible_changed (self, NULL, self->proxy); + + shell = phosh_shell_get_default (); + g_signal_connect_swapped (shell, + "notify::locked", + G_CALLBACK (on_shell_locked_changed), + self); +} + + +static void +phosh_osk_manager_dispose (GObject *object) +{ + PhoshOskManager *self = PHOSH_OSK_MANAGER (object); + + g_clear_object (&self->a11y_settings); + g_clear_object (&self->proxy); + + G_OBJECT_CLASS (phosh_osk_manager_parent_class)->dispose (object); +} + + +static void +phosh_osk_manager_class_init (PhoshOskManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_osk_manager_constructed; + object_class->dispose = phosh_osk_manager_dispose; + object_class->get_property = phosh_osk_manager_get_property; + + /** + * PhoshOskManager::available: + * + * Whether an OSK is available. That means it is registered on DBus and enabled via + * a11y setting + */ + props[PROP_AVAILABLE] = + g_param_spec_boolean ("available", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshOskManager::visible: + * + * Whether an OSK is currently visible to the user. + */ + props[PROP_VISIBLE] = + g_param_spec_boolean ("visible", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_osk_manager_init (PhoshOskManager *self) +{ + self->a11y_settings = g_settings_new ("org.gnome.desktop.a11y.applications"); + g_signal_connect_swapped (self->a11y_settings, + "changed::screen-keyboard-enabled", + G_CALLBACK (on_screen_keyboard_enabled_changed), + self); + on_screen_keyboard_enabled_changed (self); +} + + +PhoshOskManager * +phosh_osk_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_OSK_MANAGER, NULL); +} + + +gboolean +phosh_osk_manager_get_available (PhoshOskManager *self) +{ + g_return_val_if_fail (PHOSH_IS_OSK_MANAGER (self), FALSE); + + return self->has_name_owner && self->enabled; +} + + +gboolean +phosh_osk_manager_get_visible (PhoshOskManager *self) +{ + g_return_val_if_fail (PHOSH_IS_OSK_MANAGER (self), FALSE); + + return self->visible; +} + + +void +phosh_osk_manager_set_visible (PhoshOskManager *self, gboolean visible) +{ + g_return_if_fail (PHOSH_IS_OSK_MANAGER (self)); + + if (self->visible == visible) + return; + + set_visible_real (self, visible); +} diff --git a/src/osk-manager.h b/src/osk-manager.h new file mode 100644 index 000000000..9af2e80dc --- /dev/null +++ b/src/osk-manager.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_OSK_MANAGER (phosh_osk_manager_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshOskManager, phosh_osk_manager, PHOSH, OSK_MANAGER, GObject) + +PhoshOskManager * phosh_osk_manager_new (void); +gboolean phosh_osk_manager_get_available (PhoshOskManager *self); +void phosh_osk_manager_set_visible (PhoshOskManager *self, gboolean visible); +gboolean phosh_osk_manager_get_visible (PhoshOskManager *self); + +G_END_DECLS diff --git a/src/overview.c b/src/overview.c new file mode 100644 index 000000000..61aea0bc6 --- /dev/null +++ b/src/overview.c @@ -0,0 +1,903 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-overview" + +#include "phosh-config.h" + +#include "activity.h" +#include "app-grid.h" +#include "overview.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "toplevel-manager.h" +#include "toplevel-thumbnail.h" +#include "util.h" + +#include + +#include + +#define OVERVIEW_ICON_SIZE 64 + +/** + * PhoshOverview: + * + * The overview shows running apps and the app grid to launch new + * applications. + * + * The #PhoshOverview shows running apps (#PhoshActivity) and + * the app grid (#PhoshAppGrid) to launch new applications. + */ + +enum { + ACTIVITY_LAUNCHED, + ACTIVITY_RAISED, + ACTIVITY_CLOSED, + SELECTION_ABORTED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +enum { + PROP_0, + PROP_HAS_ACTIVITIES, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + + +typedef struct { + /* Running activities */ + HdyCarousel *carousel_running_activities; + GtkWidget *app_grid; + PhoshActivity *activity; + + PhoshAppTracker *app_tracker; /* unowned */ + PhoshSplashManager *splash_manager; /* unowned */ + + int has_activities; +} PhoshOverviewPrivate; + + +struct _PhoshOverview { + GtkBoxClass parent; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshOverview, phosh_overview, GTK_TYPE_BOX) + + +static PhoshToplevel *get_toplevel_from_activity (PhoshActivity *activity); +static void on_activity_clicked (PhoshOverview *self, PhoshActivity *activity); +static int get_last_app_id_pos (PhoshOverview *self, const char *app_id); + + +static PhoshActivity * +find_activity_by_app_info (PhoshOverview *self, GAppInfo *needle) +{ + g_autoptr (GList) children = NULL; + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + + children = gtk_container_get_children (GTK_CONTAINER (priv->carousel_running_activities)); + for (GList *l = children; l; l = l->next) { + PhoshActivity *activity = PHOSH_ACTIVITY (l->data); + GAppInfo *app_info = phosh_activity_get_app_info (activity); + + if (app_info && g_app_info_equal (needle, app_info)) + return activity; + } + + return NULL; +} + + +static PhoshActivity * +find_activity_by_app_id (PhoshOverview *self, const char *needle) +{ + g_autoptr (GList) children = NULL; + g_autoptr (GAppInfo) needle_info = NULL; + + g_return_val_if_fail (needle, NULL); + needle_info = G_APP_INFO (phosh_get_desktop_app_info_for_app_id (needle)); + if (!needle_info) + return NULL; + + return find_activity_by_app_info (self, needle_info); +} + + +static PhoshActivity * +create_new_activity (PhoshOverview *self, + GAppInfo *info, + PhoshToplevel *toplevel, + const char *app_id, + const char *parent_app_id) +{ + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + PhoshShell *shell = phosh_shell_get_default (); + PhoshActivity *activity; + int width, height, pos = 0; + gboolean fullscreen = FALSE, maximized = FALSE; + + phosh_shell_get_usable_area (shell, NULL, NULL, &width, &height); + + if (toplevel) { + maximized = phosh_toplevel_is_maximized (toplevel); + fullscreen = phosh_toplevel_is_fullscreen (toplevel); + } + + activity = g_object_new (PHOSH_TYPE_ACTIVITY, + "app-info", info, + "app-id", app_id, + "parent-app-id", parent_app_id, + "win-width", width, + "win-height", height, + "maximized", maximized, + "fullscreen", fullscreen, + NULL); + + g_object_connect (activity, + "swapped-signal::clicked", on_activity_clicked, self, + NULL); + + if (!toplevel) { + gboolean light_mode = !phosh_splash_manager_get_prefer_dark (priv->splash_manager); + phosh_util_toggle_style_class (GTK_WIDGET (activity), "light", light_mode); + } + + if (parent_app_id) + pos = get_last_app_id_pos (self, parent_app_id); + + if (pos) + hdy_carousel_insert (priv->carousel_running_activities, GTK_WIDGET (activity), pos); + else + gtk_container_add (GTK_CONTAINER (priv->carousel_running_activities), GTK_WIDGET (activity)); + + return activity; +} + + +static void +on_app_launch_started (PhoshOverview *self, + GAppInfo *info, + const char *startup_id, + PhoshAppTracker *app_tracker) +{ + PhoshActivity *activity; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (G_IS_APP_INFO (info)); + + g_debug ("Building splash for '%s'", g_app_info_get_id (info)); + + activity = create_new_activity (self, info, NULL, NULL, NULL); + + g_object_set_data_full (G_OBJECT (activity), + "startup-id", + g_strdup (startup_id), + g_free); +} + + +static void +on_app_ready (PhoshOverview *self, + GAppInfo *info, + const char *startup_id, + PhoshAppTracker *app_tracker) +{ + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (G_IS_APP_INFO (info)); + + g_debug ("Activity '%s' started", g_app_info_get_id (info)); +} + + +static void +on_app_failed (PhoshOverview *self, + GAppInfo *info, + const char *startup_id, + PhoshAppTracker *app_tracker) +{ + PhoshActivity *activity; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (G_IS_APP_INFO (info)); + + activity = find_activity_by_app_info (self, info); + if (!activity) { + g_debug ("Activity '%s' already gone", g_app_info_get_id (info)); + return; + } + + if (get_toplevel_from_activity (activity)) + return; + + /* TODO: show error state / notification */ + g_debug ("Activity '%s' failed to start, closing", g_app_info_get_id (info)); + gtk_widget_destroy (GTK_WIDGET (activity)); +} + + +static void +phosh_overview_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshOverview *self = PHOSH_OVERVIEW (object); + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + + switch (property_id) { + case PROP_HAS_ACTIVITIES: + g_value_set_boolean (value, priv->has_activities); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static PhoshToplevel * +get_toplevel_from_activity (PhoshActivity *activity) +{ + PhoshToplevel *toplevel; + g_return_val_if_fail (PHOSH_IS_ACTIVITY (activity), NULL); + + toplevel = g_object_get_data (G_OBJECT (activity), "toplevel"); + if (!toplevel) { + g_return_val_if_fail (!phosh_activity_get_has_thumbnail (activity), NULL); + return NULL; + } + + return toplevel; +} + + +static PhoshActivity * +find_activity_by_toplevel (PhoshOverview *self, PhoshToplevel *needle) +{ + g_autoptr (GList) children = NULL; + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + + children = gtk_container_get_children (GTK_CONTAINER (priv->carousel_running_activities)); + for (GList *l = children; l; l = l->next) { + PhoshActivity *activity = PHOSH_ACTIVITY (l->data); + PhoshToplevel *toplevel; + + toplevel = get_toplevel_from_activity (activity); + if (toplevel == needle) + return activity; + } + + g_return_val_if_reached (NULL); + return NULL; +} + + +static void +scroll_to_activity (PhoshOverview *self, PhoshActivity *activity) +{ + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + hdy_carousel_scroll_to (priv->carousel_running_activities, GTK_WIDGET (activity)); + gtk_widget_grab_focus (GTK_WIDGET (activity)); +} + + +static void +on_activity_clicked (PhoshOverview *self, PhoshActivity *activity) +{ + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + PhoshToplevel *toplevel; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + + toplevel = get_toplevel_from_activity (activity); + + if (toplevel) { + g_return_if_fail (toplevel); + + g_debug ("Will raise %s (%s)", + phosh_activity_get_app_id (activity), + phosh_toplevel_get_title (toplevel)); + + phosh_toplevel_activate (toplevel, phosh_wayland_get_wl_seat (phosh_wayland_get_default ())); + + phosh_splash_manager_lower_all (priv->splash_manager); + } else { + const char *startup_id = g_object_get_data (G_OBJECT (activity), "startup-id"); + + if (startup_id) + phosh_splash_manager_raise (priv->splash_manager, startup_id); + else + g_warning ("No startup-id for %s, can't raise splash", phosh_activity_get_app_id (activity)); + } + + g_signal_emit (self, signals[ACTIVITY_RAISED], 0); +} + + +static void +on_activity_closed (PhoshOverview *self, PhoshActivity *activity) +{ + PhoshToplevel *toplevel; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + + toplevel = g_object_get_data (G_OBJECT (activity), "toplevel"); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + g_debug ("Will close %s (%s)", + phosh_activity_get_app_id (activity), + phosh_toplevel_get_title (toplevel)); + + phosh_toplevel_close (toplevel); + phosh_trigger_feedback ("window-close"); + g_signal_emit (self, signals[ACTIVITY_CLOSED], 0); +} + + +static void +on_activity_fullscreened (PhoshOverview *self, gboolean fullscreen, PhoshActivity *activity) +{ + PhoshToplevel *toplevel; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + + toplevel = g_object_get_data (G_OBJECT (activity), "toplevel"); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + g_debug ("Fullscreen %s (%s); %d", + phosh_activity_get_app_id (activity), + phosh_toplevel_get_title (toplevel), + fullscreen); + + phosh_toplevel_fullscreen (toplevel, fullscreen); +} + + +static void +on_toplevel_closed (PhoshToplevel *toplevel, PhoshOverview *overview) +{ + PhoshActivity *activity; + PhoshOverviewPrivate *priv; + + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + g_return_if_fail (PHOSH_IS_OVERVIEW (overview)); + priv = phosh_overview_get_instance_private (overview); + + activity = find_activity_by_toplevel (overview, toplevel); + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + gtk_widget_destroy (GTK_WIDGET (activity)); + + if (priv->activity == activity) + priv->activity = NULL; +} + + +static void +on_toplevel_activated_changed (PhoshToplevel *toplevel, GParamSpec *pspec, PhoshOverview *overview) +{ + PhoshActivity *activity; + PhoshOverviewPrivate *priv; + g_return_if_fail (PHOSH_IS_OVERVIEW (overview)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + priv = phosh_overview_get_instance_private (overview); + + if (phosh_toplevel_is_activated (toplevel)) { + activity = find_activity_by_toplevel (overview, toplevel); + priv->activity = activity; + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + scroll_to_activity (overview, activity); + } +} + + +static void +on_thumbnail_ready_changed (PhoshThumbnail *thumbnail, GParamSpec *pspec, PhoshActivity *activity) +{ + g_return_if_fail (PHOSH_IS_THUMBNAIL (thumbnail)); + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + + phosh_activity_set_thumbnail (activity, thumbnail); +} + + +static void +request_thumbnail (PhoshActivity *activity, PhoshToplevel *toplevel) +{ + PhoshToplevelThumbnail *thumbnail; + GtkAllocation allocation; + int scale; + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + scale = gtk_widget_get_scale_factor (GTK_WIDGET (activity)); + phosh_activity_get_thumbnail_allocation (activity, &allocation); + thumbnail = phosh_toplevel_thumbnail_new_from_toplevel (toplevel, allocation.width * scale, + allocation.height * scale); + g_signal_connect_object (thumbnail, + "notify::ready", + G_CALLBACK (on_thumbnail_ready_changed), + activity, + 0); +} + + +static void +on_activity_resized (PhoshOverview *self, GtkAllocation *alloc, PhoshActivity *activity) +{ + PhoshToplevel *toplevel; + + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + toplevel = g_object_get_data (G_OBJECT (activity), "toplevel"); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + request_thumbnail (activity, toplevel); +} + + +static void +on_activity_has_focus_changed (PhoshOverview *self, GParamSpec *pspec, PhoshActivity *activity) +{ + PhoshOverviewPrivate *priv; + + g_return_if_fail (PHOSH_IS_ACTIVITY (activity)); + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + priv = phosh_overview_get_instance_private (self); + + if (gtk_widget_has_focus (GTK_WIDGET (activity))) + hdy_carousel_scroll_to (priv->carousel_running_activities, GTK_WIDGET (activity)); +} + + +static int +get_last_app_id_pos (PhoshOverview *self, const char *app_id) +{ + PhoshOverviewPrivate *priv; + g_autoptr (GList) children = NULL; + int pos; + + if (!app_id) + return 0; + + priv = phosh_overview_get_instance_private (self); + + children = gtk_container_get_children (GTK_CONTAINER (priv->carousel_running_activities)); + pos = g_list_length (children); + for (GList *l = g_list_last (children); l; l = l->prev) { + PhoshActivity *a = PHOSH_ACTIVITY (l->data); + + if (g_strcmp0 (phosh_activity_get_app_id (a), app_id) == 0) + break; + + pos--; + } + + return pos; +} + + +static void +toplevel_to_activity (PhoshOverview *self, PhoshToplevel *toplevel) +{ + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + PhoshActivity *activity; + const char *app_id, *title; + const char *parent_app_id = NULL; + int width, height; + PhoshToplevelManager *m = phosh_shell_get_toplevel_manager (phosh_shell_get_default ()); + PhoshToplevel *parent = NULL; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + + app_id = phosh_toplevel_get_app_id (toplevel); + title = phosh_toplevel_get_title (toplevel); + + if (phosh_toplevel_get_parent_handle (toplevel)) + parent = phosh_toplevel_manager_get_parent (m, toplevel); + if (parent) + parent_app_id = phosh_toplevel_get_app_id (parent); + + activity = find_activity_by_app_id (self, app_id); + if (activity && get_toplevel_from_activity (activity)) { + /* Multi window apps */ + g_debug ("Existing activity '%s' already has a toplevel", app_id); + activity = NULL; + } + + phosh_shell_get_usable_area (phosh_shell_get_default (), NULL, NULL, &width, &height); + if (activity) { + g_debug ("Using existing activity for '%s' (%s)", app_id, title); + g_object_set (activity, + "win-width", width, + "win-height", height, + "maximized", phosh_toplevel_is_maximized (toplevel), + "fullscreen", phosh_toplevel_is_fullscreen (toplevel), + NULL); + + g_object_set_data (G_OBJECT (activity), "startup-id", NULL); + request_thumbnail (activity, toplevel); + } else { + g_debug ("Building activator for '%s' (%s)", app_id, title); + activity = create_new_activity (self, NULL, toplevel, app_id, parent_app_id); + } + + g_object_set_data (G_OBJECT (activity), "toplevel", toplevel); + gtk_widget_set_visible (GTK_WIDGET (activity), TRUE); + + g_object_connect (activity, + "swapped-signal::closed", on_activity_closed, self, + "swapped-signal::fullscreened", on_activity_fullscreened, self, + "swapped-signal::notify::has-focus", on_activity_has_focus_changed, self, + "swapped-signal::resized", on_activity_resized, self, + NULL); + + g_object_connect (toplevel, + "object-signal::closed", on_toplevel_closed, self, + "object-signal::notify::activated", on_toplevel_activated_changed, self, + NULL); + + g_object_bind_property (toplevel, "maximized", activity, "maximized", G_BINDING_DEFAULT); + g_object_bind_property (toplevel, "fullscreen", activity, "fullscreen", G_BINDING_DEFAULT); + + if (phosh_toplevel_is_activated (toplevel)) { + scroll_to_activity (self, PHOSH_ACTIVITY (activity)); + priv->activity = PHOSH_ACTIVITY (activity); + } +} + + +static void +set_has_activities (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + gboolean has_activities; + + has_activities = !!hdy_carousel_get_n_pages (HDY_CAROUSEL (priv->carousel_running_activities)); + if (priv->has_activities == has_activities) + return; + + priv->has_activities = has_activities; + gtk_widget_set_visible (GTK_WIDGET (priv->carousel_running_activities), has_activities); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HAS_ACTIVITIES]); +} + + +static void +get_running_activities (PhoshOverview *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshToplevelManager *toplevel_manager = phosh_shell_get_toplevel_manager (shell); + guint toplevels_num = phosh_toplevel_manager_get_num_toplevels (toplevel_manager); + + set_has_activities (self); + + for (guint i = 0; i < toplevels_num; i++) { + PhoshToplevel *toplevel = phosh_toplevel_manager_get_toplevel (toplevel_manager, i); + toplevel_to_activity (self, toplevel); + } +} + + +static void +on_toplevel_added (PhoshOverview *self, PhoshToplevel *toplevel) +{ + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + toplevel_to_activity (self, toplevel); +} + + +static void +on_toplevel_changed (PhoshOverview *self, PhoshToplevel *toplevel) +{ + PhoshActivity *activity; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + if (phosh_shell_get_state (phosh_shell_get_default ()) & PHOSH_STATE_OVERVIEW) + return; + + activity = find_activity_by_toplevel (self, toplevel); + g_return_if_fail (activity); + + request_thumbnail (activity, toplevel); +} + + +static void +on_toplevel_missing (PhoshOverview *self, GAppInfo *info) +{ + PhoshActivity *activity; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (G_IS_APP_INFO (info)); + + activity = find_activity_by_app_info (self, info); + if (!activity) + return; + + /* Activity is not a splash screen, so keep it */ + if (phosh_activity_get_has_thumbnail (activity)) + return; + + g_warning ("App %s didn't present a toplevel, hiding splash", g_app_info_get_id (info)); + gtk_widget_destroy (GTK_WIDGET (activity)); +} + + +static void +on_n_pages_changed (PhoshOverview *self) +{ + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + + set_has_activities (self); +} + + +static void +phosh_overview_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + PhoshOverview *self = PHOSH_OVERVIEW (widget); + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + g_autoptr (GList) children = NULL; + int width, height; + + phosh_shell_get_usable_area (phosh_shell_get_default (), NULL, NULL, &width, &height); + children = gtk_container_get_children (GTK_CONTAINER (priv->carousel_running_activities)); + + for (GList *l = children; l; l = l->next) { + g_object_set (l->data, + "win-width", width, + "win-height", height, + NULL); + } + + GTK_WIDGET_CLASS (phosh_overview_parent_class)->size_allocate (widget, alloc); +} + + +static void +on_app_launched (PhoshOverview *self, GAppInfo *info, GtkWidget *widget) +{ + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + + g_signal_emit (self, signals[ACTIVITY_LAUNCHED], 0); +} + + +static void +on_page_changed (PhoshOverview *self, guint index, HdyCarousel *carousel) +{ + PhoshActivity *activity; + PhoshToplevel *toplevel; + g_autoptr (GList) list = NULL; + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + g_return_if_fail (HDY_IS_CAROUSEL (carousel)); + + /* Carousel is empty */ + if (((int)index < 0)) + return; + + /* don't raise on scroll in docked mode */ + if (phosh_shell_get_docked (phosh_shell_get_default ())) + return; + + /* ignore page changes when overview is not open */ + if (!(phosh_shell_get_state (phosh_shell_get_default ()) & PHOSH_STATE_OVERVIEW)) + return; + + list = gtk_container_get_children (GTK_CONTAINER (carousel)); + activity = PHOSH_ACTIVITY (g_list_nth_data (list, index)); + toplevel = get_toplevel_from_activity (activity); + phosh_toplevel_activate (toplevel, phosh_wayland_get_wl_seat (phosh_wayland_get_default ())); + + if (!gtk_widget_has_focus (GTK_WIDGET (activity))) + gtk_widget_grab_focus (GTK_WIDGET (activity)); +} + + +static void +phosh_overview_constructed (GObject *object) +{ + PhoshOverview *self = PHOSH_OVERVIEW (object); + PhoshToplevelManager *toplevel_manager = + phosh_shell_get_toplevel_manager (phosh_shell_get_default ()); + + G_OBJECT_CLASS (phosh_overview_parent_class)->constructed (object); + + g_object_connect (toplevel_manager, + "swapped-object-signal::toplevel-added", on_toplevel_added, self, + "swapped-object-signal::toplevel-changed", on_toplevel_changed, self, + "swapped-object-signal::toplevel-missing", on_toplevel_missing, self, + NULL); + + get_running_activities (self); +} + + +static void +phosh_overview_class_init (PhoshOverviewClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_overview_constructed; + object_class->get_property = phosh_overview_get_property; + widget_class->size_allocate = phosh_overview_size_allocate; + + /** + * PhoshOverview:has-activities: + * + * Whether the overview has running activities + */ + props[PROP_HAS_ACTIVITIES] = + g_param_spec_boolean ("has-activities", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[ACTIVITY_LAUNCHED] = + g_signal_new ("activity-launched", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[ACTIVITY_RAISED] = + g_signal_new ("activity-raised", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[SELECTION_ABORTED] = + g_signal_new ("selection-aborted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + signals[ACTIVITY_CLOSED] = + g_signal_new ("activity-closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /* ensure used custom types */ + g_type_ensure (PHOSH_TYPE_APP_GRID); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/overview.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshOverview, app_grid); + gtk_widget_class_bind_template_child_private (widget_class, PhoshOverview, + carousel_running_activities); + gtk_widget_class_bind_template_callback (widget_class, on_app_launched); + gtk_widget_class_bind_template_callback (widget_class, on_n_pages_changed); + gtk_widget_class_bind_template_callback (widget_class, on_page_changed); + + gtk_widget_class_set_css_name (widget_class, "phosh-overview"); +} + + +static void +phosh_overview_init (PhoshOverview *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshOverviewPrivate *priv = phosh_overview_get_instance_private (self); + + priv->has_activities = -1; + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->app_tracker = phosh_shell_get_app_tracker (shell); + /* Allow it to be empty for tests */ + if (priv->app_tracker) { + g_object_connect (priv->app_tracker, + "swapped-object-signal::app-launch-started", on_app_launch_started, self, + "swapped-object-signal::app-failed", on_app_failed, self, + "swapped-object-signal::app-ready", on_app_ready, self, + NULL); + } + + priv->splash_manager = phosh_shell_get_splash_manager (shell); +} + + +GtkWidget * +phosh_overview_new (void) +{ + return g_object_new (PHOSH_TYPE_OVERVIEW, NULL); +} + + +void +phosh_overview_refresh (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv; + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + priv = phosh_overview_get_instance_private (self); + + if (priv->activity) { + gtk_widget_grab_focus (GTK_WIDGET (priv->activity)); + request_thumbnail (priv->activity, get_toplevel_from_activity (priv->activity)); + } +} + + +void +phosh_overview_reset (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + priv = phosh_overview_get_instance_private (self); + + phosh_app_grid_reset (PHOSH_APP_GRID (priv->app_grid)); +} + + +void +phosh_overview_focus_app_search (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv; + + g_return_if_fail (PHOSH_IS_OVERVIEW (self)); + priv = phosh_overview_get_instance_private (self); + phosh_app_grid_focus_search (PHOSH_APP_GRID (priv->app_grid)); +} + + +gboolean +phosh_overview_handle_search (PhoshOverview *self, GdkEvent *event) +{ + PhoshOverviewPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_OVERVIEW (self), GDK_EVENT_PROPAGATE); + priv = phosh_overview_get_instance_private (self); + return phosh_app_grid_handle_search (PHOSH_APP_GRID (priv->app_grid), event); +} + + +gboolean +phosh_overview_has_running_activities (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_OVERVIEW (self), FALSE); + priv = phosh_overview_get_instance_private (self); + + return priv->has_activities; +} + +/** + * phosh_overview_get_app_grid: + * @self: The overview + * + * Get the application grid + * + * Returns:(transfer none): The app grid widget + */ +PhoshAppGrid * +phosh_overview_get_app_grid (PhoshOverview *self) +{ + PhoshOverviewPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_OVERVIEW (self), NULL); + priv = phosh_overview_get_instance_private (self); + + return PHOSH_APP_GRID (priv->app_grid); +} diff --git a/src/overview.h b/src/overview.h new file mode 100644 index 000000000..807df8108 --- /dev/null +++ b/src/overview.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "app-grid.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_OVERVIEW (phosh_overview_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshOverview, phosh_overview, PHOSH, OVERVIEW, GtkBox) + + +GtkWidget *phosh_overview_new (void); +void phosh_overview_refresh (PhoshOverview *self); +void phosh_overview_reset (PhoshOverview *self); +void phosh_overview_focus_app_search (PhoshOverview *self); +gboolean phosh_overview_has_running_activities (PhoshOverview *self); +gboolean phosh_overview_handle_search (PhoshOverview *self, GdkEvent *event); +PhoshAppGrid *phosh_overview_get_app_grid (PhoshOverview *self); + +G_END_DECLS diff --git a/src/password-entry.c b/src/password-entry.c new file mode 100644 index 000000000..0c8d7d1d9 --- /dev/null +++ b/src/password-entry.c @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2023 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Rudra Pratap Singh + */ + +#include "password-entry.h" + +#define PASSWORD_EYE_OPEN "eye-open-negative-filled-symbolic" +#define PASSWORD_EYE_CLOSE "eye-not-looking-symbolic" + +/** + * PhoshPasswordEntry: + * + * A widget for entering passwords + * + * Since: 0.33.0 + */ + +struct _PhoshPasswordEntry { + GtkEntry parent_instance; + + gboolean visibility; +}; + +G_DEFINE_TYPE (PhoshPasswordEntry, phosh_password_entry, GTK_TYPE_ENTRY) + + +static void +on_icon_press (PhoshPasswordEntry *self, gpointer user_data) +{ + const char *icon_name; + + self->visibility = !self->visibility; + icon_name = self->visibility ? PASSWORD_EYE_CLOSE : PASSWORD_EYE_OPEN; + + gtk_entry_set_visibility (GTK_ENTRY (self), self->visibility); + gtk_entry_set_icon_from_icon_name (GTK_ENTRY (self), + GTK_ENTRY_ICON_SECONDARY, + icon_name); +} + + +static void +phosh_password_entry_class_init (PhoshPasswordEntryClass *klass) +{ + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/password-entry.ui"); + gtk_widget_class_bind_template_callback (widget_class, on_icon_press); +} + + +static void +phosh_password_entry_init (PhoshPasswordEntry *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshPasswordEntry * +phosh_password_entry_new (void) +{ + return PHOSH_PASSWORD_ENTRY (g_object_new (PHOSH_TYPE_PASSWORD_ENTRY, NULL)); +} diff --git a/src/password-entry.h b/src/password-entry.h new file mode 100644 index 000000000..0973fe9ee --- /dev/null +++ b/src/password-entry.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2023 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_PASSWORD_ENTRY (phosh_password_entry_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshPasswordEntry, phosh_password_entry, PHOSH, PASSWORD_ENTRY, GtkEntry) + +PhoshPasswordEntry *phosh_password_entry_new (void); + +G_END_DECLS \ No newline at end of file diff --git a/src/phosh-exported-symbols.txt.in b/src/phosh-exported-symbols.txt.in new file mode 100644 index 000000000..7dd1bb69e --- /dev/null +++ b/src/phosh-exported-symbols.txt.in @@ -0,0 +1,105 @@ + phosh_shell_get_default; + phosh_shell_get_locked; + phosh_shell_get_primary_monitor; + phosh_shell_get_type; + + # Utils are useful in plugins too + phosh_util_*; + + # Plugins can inhibit session idleness + phosh_shell_get_session_manager; + phosh_session_manager_inhibit; + phosh_session_manager_uninhibit; + + # Plugins can send notifications + phosh_notification_get_type; + phosh_notify_manager_get_default; + phosh_notify_manager_get_type; + phosh_notify_manager_add_shell_notification; + + # Launcher-box plugin needs launcher entry states + phosh_shell_get_launcher_entry_manager; + + # Scaling quick setting needs monitors + phosh_monitor_get_type; + phosh_monitor_get_fractional_scale; + phosh_monitor_manager_set_monitor_scale; + phosh_monitor_manager_apply_monitor_config; + + # Night Light plugin wants to check night light support + phosh_shell_get_monitor_manager; + phosh_monitor_manager_get_type; + phosh_monitor_manager_get_night_light_supported; + + # Media player plugin + phosh_shell_get_mpris_manager; + phosh_media_player_get_type; + phosh_media_player_new; + phosh_media_player_set_player; + phosh_mpris_manager_get_type; + phosh_mpris_manager_get_known_players; + + # Wi-Fi Hotspot plugin wants Wi-Fi Manager + phosh_shell_get_wifi_manager; + phosh_wifi_manager_get_type; + phosh_wifi_manager_get_active_connection; + phosh_wifi_manager_get_enabled; + phosh_wifi_manager_get_present; + phosh_wifi_manager_get_state; + phosh_wifi_manager_is_hotspot_master; + phosh_wifi_manager_set_enabled; + phosh_wifi_manager_set_hotspot_master; + + # Mobile Data wants WWan Manager + phosh_shell_get_wwan; + phosh_wwan_has_data; + phosh_wwan_is_enabled; + phosh_wwan_is_unlocked; + phosh_wwan_set_data_enabled; + + # For custom quick setting plugins: + phosh_quick_setting_get_active; + phosh_quick_setting_get_can_show_status; + phosh_quick_setting_get_long_press_action_name; + phosh_quick_setting_get_long_press_action_target; + phosh_quick_setting_get_showing_status; + phosh_quick_setting_get_status_page; + phosh_quick_setting_get_type; + phosh_quick_setting_new; + phosh_quick_setting_set_active; + phosh_quick_setting_set_can_show_status; + phosh_quick_setting_set_long_press_action_name; + phosh_quick_setting_set_long_press_action_target; + phosh_quick_setting_set_showing_status; + phosh_quick_setting_set_status_page; + phosh_status_icon_get_extra_widget; + phosh_status_icon_get_icon_name; + phosh_status_icon_get_icon_size; + phosh_status_icon_get_pixel_size; + phosh_status_icon_get_info; + phosh_status_icon_get_type; + phosh_status_icon_new; + phosh_status_icon_set_extra_widget; + phosh_status_icon_set_icon_name; + phosh_status_icon_set_icon_size; + phosh_status_icon_set_pixel_size; + phosh_status_icon_set_info; + phosh_status_page_get_content; + phosh_status_page_get_footer; + phosh_status_page_get_header; + phosh_status_page_get_title; + phosh_status_page_get_type; + phosh_status_page_new; + phosh_status_page_placeholder_get_icon_name; + phosh_status_page_placeholder_get_title; + phosh_status_page_placeholder_get_type; + phosh_status_page_placeholder_set_icon_name; + phosh_status_page_placeholder_set_title; + phosh_status_page_set_content; + phosh_status_page_set_footer; + phosh_status_page_set_header; + phosh_status_page_set_title; + + # The plugins use our gtk-list-models: + gtk_filter_list_model_*; + gtk_sort_list_model_*; diff --git a/src/phosh-marshalers.list b/src/phosh-marshalers.list new file mode 100644 index 000000000..2f9118b98 --- /dev/null +++ b/src/phosh-marshalers.list @@ -0,0 +1,2 @@ +VOID:OBJECT,STRING +VOID:STRING,VARIANT \ No newline at end of file diff --git a/src/phosh-settings-enums.h b/src/phosh-settings-enums.h new file mode 100644 index 000000000..035a70c34 --- /dev/null +++ b/src/phosh-settings-enums.h @@ -0,0 +1,70 @@ +/* + * Copyright © 2019 Zander Brown + * 2020-2021 Purism SPC + * 2023 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +/** + * PhoshAppFilterModeFlags: + * @PHOSH_APP_FILTER_MODE_FLAGS_NONE: No filtering + * @PHOSH_APP_FILTER_MODE_FLAGS_ADAPTIVE: Only show apps in mobile mode that adapt + * to smalls screen sizes. + * + * Controls what kind of app filtering is done. +*/ +typedef enum { + PHOSH_APP_FILTER_MODE_FLAGS_NONE = 0, + PHOSH_APP_FILTER_MODE_FLAGS_ADAPTIVE = (1 << 0), +} PhoshAppFilterModeFlags; + +/** + * PhoshShellLayout: + * @PHOSH_SHELL_LAYOUT_NONE: Don't perform any additional layouting + * @PHOSH_SHELL_LAYOUT_DEVICE: Use device information to optimize layout + * + * How the shell's UI elements are laid out. + */ +typedef enum { + PHOSH_SHELL_LAYOUT_NONE = 0, + PHOSH_SHELL_LAYOUT_DEVICE = 1, +} PhoshShellLayout; + + +typedef enum { + PHOSH_LAYOUT_CLOCK_POS_CENTER = 0, + PHOSH_LAYOUT_CLOCK_POS_LEFT = 1, + PHOSH_LAYOUT_CLOCK_POS_RIGHT = 2, +} PhoshLayoutClockPosition; + +/** + * PhoshNotifyScreenWakeupFlags: + * @PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_NONE: No wakeup + * @PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_URGENCY: Wakeup screen based on notification urgency + * @PHOSH_NOTIFY_SCREEN_WAKUP_FLAG_CATEGORY: Wakeup screen based on notification category + * @PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_ANY: Wakeup screen on any notification + * + * What notification properties trigger screen wakeup + */ +typedef enum { + PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_NONE = 0, /*< skip >*/ + PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_ANY = (1 << 0), + PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_URGENCY = (1 << 1), + PHOSH_NOTIFY_SCREEN_WAKEUP_FLAG_CATEGORY = (1 << 2), +} PhoshNotifyScreenWakeupFlags; + +/** + * PhoshWWanBackend: + * @PHOSH_WWAN_BACKEND_MM: Use ModemManager + * @PHOSH_WWAN_BACKEND_OFONO: Use oFono + * + * Which WWAN backend to use. + */ +typedef enum /*< enum,prefix=PHOSH >*/ +{ + PHOSH_WWAN_BACKEND_MM, /*< nick=modemmanager >*/ + PHOSH_WWAN_BACKEND_OFONO, /*< nick=ofono >*/ +} PhoshWWanBackend; diff --git a/src/phosh-wayland.c b/src/phosh-wayland.c new file mode 100644 index 000000000..cba8a12c5 --- /dev/null +++ b/src/phosh-wayland.c @@ -0,0 +1,590 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-wayland" + +#include "phosh-config.h" +#include "phosh-enums.h" +#include "phosh-wayland.h" + +#include + +/** + * PhoshWayland: + * + * A wayland registry listener + * + * The #PhoshWayland singleton is responsible for listening to wayland + * registry events registering the objects that show up there to make + * them available to Phosh's other classes. + */ + +enum { + PHOSH_WAYLAND_PROP_0, + PHOSH_WAYLAND_PROP_WL_OUTPUTS, + PHOSH_WAYLAND_PROP_SEAT_CAPABILITIES, + PHOSH_WAYLAND_PROP_LAST_PROP, +}; +static GParamSpec *props[PHOSH_WAYLAND_PROP_LAST_PROP]; + + +#define WL_SEAT_CAPS(caps) ((caps) & 0x00FF) +#define DEVICE_STATE_CAPS(caps) ((caps) & 0xFF00) +#define DEVICE_STATE_SHIFT 8 + +struct _PhoshWayland { + GObject parent; + + struct ext_idle_notifier_v1 *ext_idle_notifier_v1; + struct phosh_private *phosh_private; + uint32_t phosh_private_version; + struct zwp_virtual_keyboard_manager_v1 *zwp_virtual_keyboard_manager_v1; + struct wl_display *display; + struct wl_registry *registry; + struct wl_seat *wl_seat; + struct xdg_wm_base *xdg_wm_base; + struct zwlr_foreign_toplevel_manager_v1 *zwlr_foreign_toplevel_manager_v1; + struct zwlr_gamma_control_manager_v1 *zwlr_gamma_control_manager_v1; + struct zwlr_layer_shell_v1 *layer_shell; + struct zwlr_output_manager_v1 *zwlr_output_manager_v1; + struct zwlr_output_power_manager_v1 *zwlr_output_power_manager_v1; + struct zxdg_output_manager_v1 *zxdg_output_manager_v1; + struct zwlr_screencopy_manager_v1 *zwlr_screencopy_manager_v1; + struct zphoc_layer_shell_effects_v1 *zphoc_layer_shell_effects_v1; + struct zphoc_device_state_v1 *zphoc_device_state_v1; + struct wl_shm *wl_shm; + GHashTable *wl_outputs; + PhoshWaylandSeatCapabilities seat_capabilities; +}; + +G_DEFINE_TYPE (PhoshWayland, phosh_wayland, G_TYPE_OBJECT) + + +static void +registry_handle_global (void *data, + struct wl_registry *registry, + uint32_t name, + const char *interface, + uint32_t version) +{ + PhoshWayland *self = data; + struct wl_output *output; + + if (!strcmp (interface, "phosh_private")) { + self->phosh_private = wl_registry_bind (registry, + name, + &phosh_private_interface, + MIN (7, version)); + self->phosh_private_version = version; + } else if (!strcmp (interface, zphoc_layer_shell_effects_v1_interface.name)) { + self->zphoc_layer_shell_effects_v1 = wl_registry_bind (registry, + name, + &zphoc_layer_shell_effects_v1_interface, + MIN (3, version)); + } else if (!strcmp (interface, zphoc_device_state_v1_interface.name)) { + self->zphoc_device_state_v1 = wl_registry_bind (registry, + name, + &zphoc_device_state_v1_interface, + 1); + } else if (!strcmp (interface, zwlr_layer_shell_v1_interface.name)) { + self->layer_shell = wl_registry_bind (registry, + name, + &zwlr_layer_shell_v1_interface, + 2); + } else if (!strcmp (interface, "wl_output")) { + output = wl_registry_bind (registry, name, &wl_output_interface, 2); + g_debug ("Got new output %p", output); + g_hash_table_insert (self->wl_outputs, GINT_TO_POINTER (name), output); + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_WAYLAND_PROP_WL_OUTPUTS]); + } else if (!strcmp (interface, "wl_seat")) { + self->wl_seat = wl_registry_bind (registry, name, &wl_seat_interface, 1); + } else if (!strcmp (interface, "wl_shm")) { + self->wl_shm = wl_registry_bind (registry, name, &wl_shm_interface, 1); + } else if (!strcmp (interface, xdg_wm_base_interface.name)) { + self->xdg_wm_base = wl_registry_bind (registry, name, &xdg_wm_base_interface, 1); + } else if (!strcmp (interface, zwlr_gamma_control_manager_v1_interface.name)) { + self->zwlr_gamma_control_manager_v1 = wl_registry_bind (registry, + name, + &zwlr_gamma_control_manager_v1_interface, + 1); + } else if (!strcmp (interface, zxdg_output_manager_v1_interface.name)) { + self->zxdg_output_manager_v1 = wl_registry_bind (registry, + name, + &zxdg_output_manager_v1_interface, + 3); + } else if (!strcmp (interface, zwlr_output_manager_v1_interface.name)) { + self->zwlr_output_manager_v1 = wl_registry_bind (registry, + name, + &zwlr_output_manager_v1_interface, + MIN (2, version)); + } else if (!strcmp (interface, zwlr_output_power_manager_v1_interface.name)) { + self->zwlr_output_power_manager_v1 = wl_registry_bind (registry, + name, + &zwlr_output_power_manager_v1_interface, + 1); + } else if (!strcmp (interface, zwlr_foreign_toplevel_manager_v1_interface.name)) { + self->zwlr_foreign_toplevel_manager_v1 = + wl_registry_bind (registry, + name, + &zwlr_foreign_toplevel_manager_v1_interface, + 3); + } else if (!strcmp (interface, zwlr_screencopy_manager_v1_interface.name)) { + self->zwlr_screencopy_manager_v1 = wl_registry_bind (registry, + name, + &zwlr_screencopy_manager_v1_interface, + 2); + } else if (!strcmp (interface, zwp_virtual_keyboard_manager_v1_interface.name)) { + self->zwp_virtual_keyboard_manager_v1 = + wl_registry_bind (registry, + name, + &zwp_virtual_keyboard_manager_v1_interface, + 1); + } else if (!strcmp (interface, ext_idle_notifier_v1_interface.name)) { + self->ext_idle_notifier_v1 = wl_registry_bind (registry, + name, + &ext_idle_notifier_v1_interface, + 1); + } +} + + +static void +registry_handle_global_remove (void *data, + struct wl_registry *registry, + uint32_t name) +{ + PhoshWayland *self = data; + struct wl_output *wl_output; + + wl_output = g_hash_table_lookup (self->wl_outputs, GINT_TO_POINTER (name)); + if (wl_output) { + g_debug ("Output %d removed", name); + g_hash_table_remove (self->wl_outputs, GINT_TO_POINTER (name)); + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_WAYLAND_PROP_WL_OUTPUTS]); + } else { + g_warning ("Global %d removed but not handled", name); + } +} + + +static const struct wl_registry_listener registry_listener = { + registry_handle_global, + registry_handle_global_remove +}; + + +static void +phosh_wayland_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshWayland *self = PHOSH_WAYLAND (object); + + switch (property_id) { + case PHOSH_WAYLAND_PROP_WL_OUTPUTS: + g_value_set_boxed (value, self->wl_outputs); + break; + case PHOSH_WAYLAND_PROP_SEAT_CAPABILITIES: + g_value_set_flags (value, self->seat_capabilities); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +seat_handle_capabilities (void *data, struct wl_seat *wl_seat, uint32_t capabilities) +{ + PhoshWayland *self = PHOSH_WAYLAND (data); + + if (WL_SEAT_CAPS (self->seat_capabilities) != capabilities) { + g_debug ("Seat capabilities: 0x%x", capabilities); + self->seat_capabilities = DEVICE_STATE_CAPS (self->seat_capabilities) | capabilities; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_WAYLAND_PROP_SEAT_CAPABILITIES]); + } +} + + +static void +seat_handle_name (void *data, struct wl_seat *wl_seat, const char *name) +{ + /* nothing to do */ +} + + +static const struct wl_seat_listener seat_listener = { + seat_handle_capabilities, + seat_handle_name, +}; + + +static void +device_state_handle_capabilities (void *data, + struct zphoc_device_state_v1 *zphoc_device_state_v1, + uint32_t capabilities) +{ + PhoshWayland *self = PHOSH_WAYLAND (data); + + g_debug ("Device state capabilities: 0x%x", capabilities); + + g_return_if_fail (PHOSH_IS_WAYLAND (self)); + + if ((DEVICE_STATE_CAPS (self->seat_capabilities) >> DEVICE_STATE_SHIFT) != capabilities) { + self->seat_capabilities = + WL_SEAT_CAPS (self->seat_capabilities) | (capabilities << DEVICE_STATE_SHIFT); + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_WAYLAND_PROP_SEAT_CAPABILITIES]); + } +} + + +static const struct zphoc_device_state_v1_listener device_state_listener = { + .capabilities = device_state_handle_capabilities, +}; + + +static void +phosh_wayland_constructed (GObject *object) +{ + PhoshWayland *self = PHOSH_WAYLAND (object); + guint num_outputs; + GdkDisplay *gdk_display; + + G_OBJECT_CLASS (phosh_wayland_parent_class)->constructed (object); + + gdk_set_allowed_backends ("wayland"); + gdk_display = gdk_display_get_default (); + self->display = gdk_wayland_display_get_wl_display (gdk_display); + + if (self->display == NULL) + g_error ("Failed to get display: %m\n"); + + self->registry = wl_display_get_registry (self->display); + wl_registry_add_listener (self->registry, ®istry_listener, self); + + /* Wait until we have been notified about the wayland globals we require */ + phosh_wayland_roundtrip (self); + num_outputs = g_hash_table_size (self->wl_outputs); + if (!num_outputs || !self->layer_shell || !self->ext_idle_notifier_v1 || + !self->xdg_wm_base || + !self->zxdg_output_manager_v1 || + !self->zwlr_output_power_manager_v1 || + !self->zphoc_layer_shell_effects_v1) { + g_error ("Wayland compositor lacks needed globals\n" + "outputs: %d, layer_shell: %p, idle_manager: %p, " + "xdg_wm: %p, " + "xdg_output: %p, wlr_output_manager: %p, " + "wlr_foreign_toplevel_manager: %p, " + "zwlr_output_power_manager_v1: %p, " + "zphoc_layer_shell_effects_v1: %p" + "\n", + num_outputs, self->layer_shell, self->ext_idle_notifier_v1, + self->xdg_wm_base, + self->zxdg_output_manager_v1, + self->zwlr_output_manager_v1, + self->zwlr_foreign_toplevel_manager_v1, + self->zwlr_output_power_manager_v1, + self->zphoc_layer_shell_effects_v1); + } + if (!self->phosh_private) + g_info ("Could not find phosh private interface, disabling some features"); + + wl_seat_add_listener (self->wl_seat, &seat_listener, self); + + if (self->zphoc_device_state_v1) + zphoc_device_state_v1_add_listener (self->zphoc_device_state_v1, &device_state_listener, self); + else + g_info ("Phoc doesn't support zphoc_device_state_v1 - please upgrade"); +} + + +static void +phosh_wayland_dispose (GObject *object) +{ + PhoshWayland *self = PHOSH_WAYLAND (object); + + g_clear_pointer (&self->ext_idle_notifier_v1, ext_idle_notifier_v1_destroy); + g_clear_pointer (&self->layer_shell, &zwlr_layer_shell_v1_destroy); + g_clear_pointer (&self->phosh_private, phosh_private_destroy); + g_clear_pointer (&self->registry, wl_registry_destroy); + g_clear_pointer (&self->wl_seat, wl_seat_destroy); + g_clear_pointer (&self->wl_shm, wl_shm_destroy); + g_clear_pointer (&self->xdg_wm_base, xdg_wm_base_destroy); + g_clear_pointer (&self->zwlr_foreign_toplevel_manager_v1, + zwlr_foreign_toplevel_manager_v1_destroy); + g_clear_pointer (&self->zwlr_gamma_control_manager_v1, zwlr_gamma_control_manager_v1_destroy); + g_clear_pointer (&self->zwlr_output_manager_v1, zwlr_output_manager_v1_destroy); + g_clear_pointer (&self->zwlr_output_power_manager_v1, zwlr_output_power_manager_v1_destroy); + g_clear_pointer (&self->zwlr_screencopy_manager_v1, zwlr_screencopy_manager_v1_destroy); + g_clear_pointer (&self->zwp_virtual_keyboard_manager_v1, zwp_virtual_keyboard_manager_v1_destroy); + g_clear_pointer (&self->zxdg_output_manager_v1, zxdg_output_manager_v1_destroy); + g_clear_pointer (&self->zphoc_layer_shell_effects_v1, zphoc_layer_shell_effects_v1_destroy); + g_clear_pointer (&self->zphoc_device_state_v1, zphoc_device_state_v1_destroy); + + g_clear_pointer (&self->wl_outputs, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_wayland_parent_class)->dispose (object); +} + + +static void +phosh_wayland_class_init (PhoshWaylandClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_wayland_constructed; + object_class->dispose = phosh_wayland_dispose; + + object_class->get_property = phosh_wayland_get_property; + + props[PHOSH_WAYLAND_PROP_WL_OUTPUTS] = + g_param_spec_boxed ("wl-outputs", + "Wayland Outputs", + "The currently known wayland outputs", + G_TYPE_HASH_TABLE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PHOSH_WAYLAND_PROP_SEAT_CAPABILITIES] = + g_param_spec_flags ("seat-capabilities", + "Seat capabilities", + "The current seat capabilities", + PHOSH_TYPE_WAYLAND_SEAT_CAPABILITIES, + PHOSH_WAYLAND_SEAT_CAPABILITY_NONE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PHOSH_WAYLAND_PROP_LAST_PROP, props); +} + + +static void +output_destroy (gpointer data) +{ + struct wl_output *output = data; + + wl_output_destroy (output); +} + + +static void +phosh_wayland_init (PhoshWayland *self) +{ + self->wl_outputs = g_hash_table_new_full (g_direct_hash,g_direct_equal, NULL, output_destroy); +} + +/** + * phosh_wayland_get_default: + * + * Get the Wayland singleton for handling Wayland protocol + * interactions + * + * Returns:(transfer none): The Wayland singleton + */ +PhoshWayland * +phosh_wayland_get_default (void) +{ + static PhoshWayland *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_WAYLAND, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} + + +struct zwlr_layer_shell_v1 * +phosh_wayland_get_zwlr_layer_shell_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->layer_shell; +} + + +struct zwlr_gamma_control_manager_v1* +phosh_wayland_get_zwlr_gamma_control_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwlr_gamma_control_manager_v1; +} + + +struct wl_seat* +phosh_wayland_get_wl_seat (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->wl_seat; +} + + +struct xdg_wm_base* +phosh_wayland_get_xdg_wm_base (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->xdg_wm_base; +} + + +struct ext_idle_notifier_v1 * +phosh_wayland_get_ext_idle_notifier_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->ext_idle_notifier_v1; +} + + +struct phosh_private* +phosh_wayland_get_phosh_private (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->phosh_private; +} + + +uint32_t +phosh_wayland_get_phosh_private_version (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), 0); + + return self->phosh_private_version; +} + + +struct wl_shm* +phosh_wayland_get_wl_shm (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->wl_shm; +} + + +struct zxdg_output_manager_v1* +phosh_wayland_get_zxdg_output_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zxdg_output_manager_v1; +} + + +struct zwlr_output_manager_v1* +phosh_wayland_get_zwlr_output_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwlr_output_manager_v1; +} + +struct zwlr_output_power_manager_v1* +phosh_wayland_get_zwlr_output_power_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwlr_output_power_manager_v1; +} + +struct zwlr_foreign_toplevel_manager_v1* +phosh_wayland_get_zwlr_foreign_toplevel_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwlr_foreign_toplevel_manager_v1; +} + + +struct zwlr_screencopy_manager_v1* +phosh_wayland_get_zwlr_screencopy_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwlr_screencopy_manager_v1; +} + +struct zwp_virtual_keyboard_manager_v1* +phosh_wayland_get_zwp_virtual_keyboard_manager_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zwp_virtual_keyboard_manager_v1; +} + + +/** + * phosh_wayland_get_wl_outputs: + * @self: The #PhoshWayland singleton + * + * Returns: (transfer none): A list of outputs as a #GHashTable + * keyed by the output's name with wl_output's as values. + */ +GHashTable * +phosh_wayland_get_wl_outputs (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->wl_outputs; +} + +gboolean +phosh_wayland_has_wl_output (PhoshWayland *self, struct wl_output *wl_output) +{ + GHashTableIter iter; + gpointer key, value; + + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), FALSE); + + g_hash_table_iter_init (&iter, self->wl_outputs); + while (g_hash_table_iter_next (&iter, &key, &value)) { + if ((struct wl_output *) value == wl_output) + return TRUE; + } + return FALSE; +} + + +void +phosh_wayland_roundtrip (PhoshWayland *self) +{ + g_return_if_fail (PHOSH_IS_WAYLAND (self)); + + wl_display_roundtrip (self->display); +} + + +PhoshWaylandSeatCapabilities +phosh_wayland_get_seat_capabilities (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), PHOSH_WAYLAND_SEAT_CAPABILITY_NONE); + + return self->seat_capabilities; +} + + +struct zphoc_layer_shell_effects_v1 * +phosh_wayland_get_zphoc_layer_shell_effects_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zphoc_layer_shell_effects_v1; +} + + +struct zphoc_device_state_v1 * +phosh_wayland_get_zphoc_device_state_v1 (PhoshWayland *self) +{ + g_return_val_if_fail (PHOSH_IS_WAYLAND (self), NULL); + + return self->zphoc_device_state_v1; +} diff --git a/src/phosh-wayland.h b/src/phosh-wayland.h new file mode 100644 index 000000000..baf52b463 --- /dev/null +++ b/src/phosh-wayland.h @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ +#pragma once + +#include "ext-idle-notify-v1-client-protocol.h" +#include "virtual-keyboard-unstable-v1-client-protocol.h" +#include "phoc-device-state-unstable-v1-client-protocol.h" +#include "phoc-layer-shell-effects-unstable-v1-client-protocol.h" +#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" +#include "wlr-layer-shell-unstable-v1-client-protocol.h" +#include "wlr-gamma-control-unstable-v1-client-protocol.h" +#include "wlr-output-management-unstable-v1-client-protocol.h" +#include "wlr-output-power-management-unstable-v1-client-protocol.h" +#include "wlr-screencopy-unstable-v1-client-protocol.h" +#include "wlr-screencopy-unstable-v1-client-protocol.h" +#include "xdg-output-unstable-v1-client-protocol.h" +#include "xdg-shell-client-protocol.h" + + +/* This goes past the other wl protocols since it might need their structs */ +#include "phosh-private-client-protocol.h" + +#include + +G_BEGIN_DECLS + +/** + * PhoshWaylandSeatCapabilities: + * @PHOSH_WAYLAND_SEAT_CAPABILITY_NONE: no device detected + * @PHOSH_WAYLAND_SEAT_CAPABILITY_POINTER: the seat has pointer devices + * @PHOSH_WAYLAND_SEAT_CAPABILITY_KEYBOARD: the seat has one or more keyboards + * @PHOSH_WAYLAND_SEAT_CAPABILITY_TOUCH: the seat has touch devices + * @PHOSH_WAYLAND_SEAT_CAPABILITY_TABLET_MODE_SWITCH: the seat has a tablet mode switch + * @PHOSH_WAYLAND_SEAT_CAPABILITY_LID_SWITCH: the seat has a lid switch + * + * These match wl_seat_capabilities + */ +typedef enum { + /* From wl_seat */ + PHOSH_WAYLAND_SEAT_CAPABILITY_NONE = 0, + PHOSH_WAYLAND_SEAT_CAPABILITY_POINTER = (1 << 0), + PHOSH_WAYLAND_SEAT_CAPABILITY_KEYBOARD = (1 << 1), + PHOSH_WAYLAND_SEAT_CAPABILITY_TOUCH = (1 << 2), + /* From device_state */ + PHOSH_WAYLAND_SEAT_CAPABILITY_TABLET_MODE_SWITCH = (1 << 8), + PHOSH_WAYLAND_SEAT_CAPABILITY_LID_SWITCH = (1 << 9), +} PhoshWaylandSeatCapabilities; + +#define PHOSH_TYPE_WAYLAND phosh_wayland_get_type() + +G_DECLARE_FINAL_TYPE (PhoshWayland, phosh_wayland, PHOSH, WAYLAND, GObject) + +PhoshWayland *phosh_wayland_get_default (void); +GHashTable *phosh_wayland_get_wl_outputs (PhoshWayland *self); +gboolean phosh_wayland_has_wl_output (PhoshWayland *self, + struct wl_output *wl_output); +struct ext_idle_notifier_v1 *phosh_wayland_get_ext_idle_notifier_v1 (PhoshWayland *self); +struct phosh_private *phosh_wayland_get_phosh_private (PhoshWayland *self); +uint32_t phosh_wayland_get_phosh_private_version (PhoshWayland *self); +struct wl_seat *phosh_wayland_get_wl_seat (PhoshWayland *self); +struct wl_shm *phosh_wayland_get_wl_shm (PhoshWayland *self); +struct xdg_wm_base *phosh_wayland_get_xdg_wm_base (PhoshWayland *self); +struct zwlr_foreign_toplevel_manager_v1 *phosh_wayland_get_zwlr_foreign_toplevel_manager_v1 (PhoshWayland *self); +struct zwlr_layer_shell_v1 *phosh_wayland_get_zwlr_layer_shell_v1 (PhoshWayland *self); +struct zwlr_gamma_control_manager_v1 *phosh_wayland_get_zwlr_gamma_control_manager_v1 (PhoshWayland *self); +struct zwlr_output_manager_v1 *phosh_wayland_get_zwlr_output_manager_v1 (PhoshWayland *self); +struct zwlr_output_power_manager_v1 *phosh_wayland_get_zwlr_output_power_manager_v1 (PhoshWayland *self); +struct zxdg_output_manager_v1 *phosh_wayland_get_zxdg_output_manager_v1 (PhoshWayland *self); +struct zwlr_screencopy_manager_v1 *phosh_wayland_get_zwlr_screencopy_manager_v1 (PhoshWayland *self); +struct zwp_virtual_keyboard_manager_v1 *phosh_wayland_get_zwp_virtual_keyboard_manager_v1 (PhoshWayland *self); +void phosh_wayland_roundtrip (PhoshWayland *self); +PhoshWaylandSeatCapabilities phosh_wayland_get_seat_capabilities (PhoshWayland *self); +struct zphoc_layer_shell_effects_v1 *phosh_wayland_get_zphoc_layer_shell_effects_v1 (PhoshWayland *self); +struct zphoc_device_state_v1 *phosh_wayland_get_zphoc_device_state_v1 (PhoshWayland *self); +G_END_DECLS diff --git a/src/phosh.gresources.xml b/src/phosh.gresources.xml new file mode 100644 index 000000000..7a22d0f1f --- /dev/null +++ b/src/phosh.gresources.xml @@ -0,0 +1,101 @@ + + + + ui/activity.ui + ui/app-auth-prompt.ui + ui/app-grid-base-button.ui + ui/app-grid-button.ui + ui/app-grid-folder-button.ui + ui/app-grid.ui + ui/audio-device-row.ui + ui/audio-settings.ui + ui/brightness-settings.ui + ui/bt-device-row.ui + ui/bt-status-page.ui + ui/call-notification.ui + ui/cell-broadcast-prompt.ui + ui/channel-bar.ui + ui/emergency-contact-row.ui + ui/emergency-menu.ui + ui/end-session-dialog.ui + ui/feedback-status-page.ui + ui/gtk-mount-prompt.ui + ui/home.ui + ui/keypad.ui + ui/lockscreen.ui + ui/media-player.ui + ui/network-auth-prompt.ui + ui/notification-content.ui + ui/notification-frame.ui + ui/osd-window.ui + ui/overview.ui + ui/password-entry.ui + ui/polkit-auth-prompt.ui + ui/power-menu.ui + ui/quick-setting.ui + ui/quick-settings-box.ui + ui/quick-settings.ui + ui/revealer.ui + ui/run-command-dialog.ui + ui/settings.ui + ui/splash.ui + ui/status-icon.ui + ui/status-page.ui + ui/status-page-placeholder.ui + ui/system-modal-dialog.ui + ui/system-prompt.ui + ui/timestamp-label.ui + ui/top-panel.ui + ui/widget-box.ui + ui/wifi-network-row.ui + ui/wifi-status-page.ui + stylesheet/adwaita-dark.css + stylesheet/adwaita-hc-light.css + stylesheet/common.css + + + ../data/icons/app-icon-unknown.svg + ../data/icons/app-icon-unknown-symbolic.svg + + + ../data/icons/app-close-symbolic.svg + ../data/icons/asterisk-symbolic.svg + ../data/icons/audio-handsfree-symbolic.svg + ../data/icons/auth-sim-locked-symbolic.svg + ../data/icons/auth-sim-missing-symbolic.svg + ../data/icons/auto-brightness-symbolic.svg + ../data/icons/camera-hardware-disabled-symbolic.svg + ../data/icons/chat-symbolic.svg + ../data/icons/chat-none-symbolic.svg + ../data/icons/eye-not-looking-symbolic.svg + ../data/icons/eye-open-negative-filled-symbolic.svg + ../data/icons/feedback-quiet-symbolic.svg + ../data/icons/input-powerbar-symbolic.svg + ../data/icons/microphone-hardware-disabled-symbolic.svg + ../data/icons/mobile-data-symbolic.svg + ../data/icons/mobile-data-disabled-symbolic.svg + ../data/icons/moon-filled-symbolic.svg + ../data/icons/network-cellular-disabled-symbolic.svg + ../data/icons/network-cellular-no-data-signal-excellent-symbolic.svg + ../data/icons/network-cellular-no-data-signal-good-symbolic.svg + ../data/icons/network-cellular-no-data-signal-none-symbolic.svg + ../data/icons/network-cellular-no-data-signal-ok-symbolic.svg + ../data/icons/network-cellular-no-data-signal-weak-symbolic.svg + ../data/icons/network-vpn-disabled-symbolic.svg + ../data/icons/network-wireless-disabled-symbolic.svg + ../data/icons/no-notifications-symbolic.svg + ../data/icons/padlock-symbolic.svg + ../data/icons/phone-docked-symbolic.svg + ../data/icons/phone-undocked-symbolic.svg + ../data/icons/screen-rotation-landscape-symbolic.svg + ../data/icons/screen-rotation-portrait-symbolic.svg + ../data/icons/screenshot-portrait-symbolic.svg + ../data/icons/settings-symbolic.svg + ../data/icons/skip-backwards-10-symbolic.svg + ../data/icons/skip-forward-30-symbolic.svg + ../data/icons/splash-process-working-symbolic.svg + ../data/icons/swipe-arrow-symbolic.svg + ../data/icons/torch-disabled-symbolic.svg + ../data/icons/torch-enabled-symbolic.svg + + diff --git a/src/plugin-loader.c b/src/plugin-loader.c new file mode 100644 index 000000000..400c88668 --- /dev/null +++ b/src/plugin-loader.c @@ -0,0 +1,220 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-plugin-loader" + +#include "phosh-config.h" + +#include "plugin-loader.h" + +#include +#include + +enum { + PROP_0, + PROP_PLUGIN_DIRS, + PROP_EXTENSION_POINT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +/** + * PhoshPluginLoader: + * + * Loads plugins for a given extension point + * + * Since: 0.21.0 + */ + +struct _PhoshPluginLoader { + GObject parent; + + GStrv plugin_dirs; + char *extension_point; +}; + +G_DEFINE_TYPE (PhoshPluginLoader, phosh_plugin_loader, G_TYPE_OBJECT) + +static void +phosh_plugin_loader_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshPluginLoader *self = PHOSH_PLUGIN_LOADER (object); + + switch (property_id) { + case PROP_PLUGIN_DIRS: + g_strfreev (self->plugin_dirs); + self->plugin_dirs = g_value_dup_boxed (value); + break; + case PROP_EXTENSION_POINT: + g_free (self->extension_point); + self->extension_point = g_value_dup_string (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_plugin_loader_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshPluginLoader *self = PHOSH_PLUGIN_LOADER (object); + + switch (property_id) { + case PROP_PLUGIN_DIRS: + g_value_set_boxed (value, phosh_plugin_loader_get_plugin_dirs (self)); + break; + case PROP_EXTENSION_POINT: + g_value_set_string (value, phosh_plugin_loader_get_extension_point (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_plugin_loader_constructed (GObject *object) +{ + PhoshPluginLoader *self = PHOSH_PLUGIN_LOADER (object); + GIOExtensionPoint *ep; + + G_OBJECT_CLASS (phosh_plugin_loader_parent_class)->constructed (object); + + if (!g_module_supported ()) { + g_warning ("GModules are not supported on your platform!"); + return; + } + + ep = g_io_extension_point_register (self->extension_point); + /* TODO: Doesn't necessarily make sense for all plugins */ + g_io_extension_point_set_required_type (ep, GTK_TYPE_WIDGET); + + for (int i = 0; i < g_strv_length (self->plugin_dirs); i++) { + g_debug ("Will load plugins from '%s' for '%s'", self->plugin_dirs[i], self->extension_point); + g_io_modules_scan_all_in_directory (self->plugin_dirs[i]); + } +} + + +static void +phosh_plugin_loader_dispose (GObject *object) +{ + PhoshPluginLoader *self = PHOSH_PLUGIN_LOADER (object); + + g_clear_pointer (&self->plugin_dirs, g_strfreev); + g_clear_pointer (&self->extension_point, g_free); + + G_OBJECT_CLASS (phosh_plugin_loader_parent_class)->dispose (object); +} + + +static void +phosh_plugin_loader_class_init (PhoshPluginLoaderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_plugin_loader_get_property; + object_class->set_property = phosh_plugin_loader_set_property; + object_class->constructed = phosh_plugin_loader_constructed; + object_class->dispose = phosh_plugin_loader_dispose; + + /** + * PhoshPluginLoader:plugin-dirs: + * + * The directory to search for plugins + */ + props[PROP_PLUGIN_DIRS] = + g_param_spec_boxed ("plugin-dirs", "", "", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshPluginLoader:extension-point: + * + * The name of the extension point to load plugins for. + */ + props[PROP_EXTENSION_POINT] = + g_param_spec_string ("extension-point", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_plugin_loader_init (PhoshPluginLoader *self) +{ +} + + +PhoshPluginLoader * +phosh_plugin_loader_new (GStrv plugin_dirs, const char *extension_point) +{ + return g_object_new (PHOSH_TYPE_PLUGIN_LOADER, + "plugin-dirs", plugin_dirs, + "extension-point", extension_point, + NULL); +} + +/** + * phosh_plugin_loader_load_plugin: + * @self: The plugin loader + * @name: The name of the plugin to load + * + * Load the given plugin. + * + * Returns:(transfer floating): The plugin widget + */ +GtkWidget * +phosh_plugin_loader_load_plugin (PhoshPluginLoader *self, const char *name) +{ + GIOExtensionPoint *ep; + GIOExtension *extension; + GType type; + + g_return_val_if_fail (PHOSH_IS_PLUGIN_LOADER (self), NULL); + g_return_val_if_fail (name, NULL); + + ep = g_io_extension_point_lookup (self->extension_point); + + extension = g_io_extension_point_get_extension_by_name (ep, name); + if (extension == NULL) + return NULL; + + g_debug ("Loading plugin %s", name); + type = g_io_extension_get_type (extension); + return g_object_new (type, NULL); +} + + +const char * +phosh_plugin_loader_get_extension_point (PhoshPluginLoader *self) +{ + g_return_val_if_fail (PHOSH_IS_PLUGIN_LOADER (self), NULL); + + return self->extension_point; +} + + +const char * const * +phosh_plugin_loader_get_plugin_dirs (PhoshPluginLoader *self) +{ + g_return_val_if_fail (PHOSH_IS_PLUGIN_LOADER (self), NULL); + + return (const char *const *)self->plugin_dirs; +} diff --git a/src/plugin-loader.h b/src/plugin-loader.h new file mode 100644 index 000000000..3ebae2060 --- /dev/null +++ b/src/plugin-loader.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/* Extension point names */ +#define PHOSH_EXTENSION_POINT_LOCKSCREEN_WIDGET "phosh-lockscreen-widget" +#define PHOSH_EXTENSION_POINT_QUICK_SETTING_WIDGET "phosh-quick-setting-widget" + +#define PHOSH_TYPE_PLUGIN_LOADER phosh_plugin_loader_get_type() + +G_DECLARE_FINAL_TYPE (PhoshPluginLoader, phosh_plugin_loader, PHOSH, PLUGIN_LOADER, GObject) + +PhoshPluginLoader *phosh_plugin_loader_new (GStrv plugin_dirs, const char *extension_point); +GtkWidget *phosh_plugin_loader_load_plugin (PhoshPluginLoader *self, const char *name); +const char *phosh_plugin_loader_get_extension_point (PhoshPluginLoader *self); +const char *const *phosh_plugin_loader_get_plugin_dirs (PhoshPluginLoader *self); + +G_END_DECLS diff --git a/src/plugin-shell.h b/src/plugin-shell.h new file mode 100644 index 000000000..f4fd12df0 --- /dev/null +++ b/src/plugin-shell.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "launcher-entry-manager.h" +#include "monitor-manager.h" +#include "mpris-manager.h" +#include "session-manager.h" +#include "shell.h" +#include "wifi-manager.h" +#include "wwan/phosh-wwan-iface.h" + +G_BEGIN_DECLS + +/* Created by the shell on startup */ +PhoshLauncherEntryManager *phosh_shell_get_launcher_entry_manager (PhoshShell *self); +PhoshMonitorManager *phosh_shell_get_monitor_manager (PhoshShell *self); +PhoshMprisManager *phosh_shell_get_mpris_manager (PhoshShell *self); +PhoshSessionManager *phosh_shell_get_session_manager (PhoshShell *self); +/* Created on the fly */ +PhoshWifiManager *phosh_shell_get_wifi_manager (PhoshShell *self); +PhoshWWan *phosh_shell_get_wwan (PhoshShell *self); + +PhoshMonitor *phosh_shell_get_primary_monitor (PhoshShell *self); +PhoshMonitor *phosh_shell_get_builtin_monitor (PhoshShell *self); + +G_END_DECLS diff --git a/src/polkit-auth-agent.c b/src/polkit-auth-agent.c new file mode 100644 index 000000000..de230296c --- /dev/null +++ b/src/polkit-auth-agent.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-polkit-auth-agent" + +#include "phosh-config.h" + +#include "polkit-auth-agent.h" +#include "polkit-auth-prompt.h" +#include "shell-priv.h" + +#include +#include + +#ifdef PHOSH_POLKIT_AUTH_DEBUG +#define auth_debug(...) g_debug (__VA_ARGS__) +#else +static void +auth_debug (const char *str, ...) +{ +} +#endif + +/** + * PhoshPolkitAuthAgent: + * + * PolicyKit Authentication Agent + * + * The #PhoshPolkitAuthAgent is responsible for handling policy kit + * interaction so the shell can work as a authentication agent. + */ + +typedef struct { + /* not holding ref */ + PhoshPolkitAuthAgent *agent; + GCancellable *cancellable; + gulong handler_id; + + /* copies */ + char *action_id; + char *message; + char *icon_name; + PolkitDetails *details; + char *cookie; + GList *identities; + + GTask *simple; +} AuthRequest; + +struct _PhoshPolkitAuthAgent { + PolkitAgentListener parent; + + GList *scheduled_requests; + AuthRequest *current_request; + PhoshPolkitAuthPrompt *current_prompt; + + gpointer handle; + GCancellable *cancellable; +}; +G_DEFINE_TYPE (PhoshPolkitAuthAgent, phosh_polkit_auth_agent, POLKIT_AGENT_TYPE_LISTENER); + +static void auth_request_complete (AuthRequest *request, gboolean dismissed); + + +static void +agent_register_thread (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + PolkitAgentListener *self = POLKIT_AGENT_LISTENER (source_object); + gpointer handle; + GError *err = NULL; + g_autoptr (PolkitSubject) subject = NULL; + + subject = polkit_unix_session_new_for_process_sync (getpid (), + cancellable, + &err); + if (subject == NULL) { + if (g_str_has_prefix (err->message, "No session for pid")) + g_message ("PolKit failed to properly get our session: %s", err->message); + else + g_warning ("PolKit failed to properly get our session: %s", err->message); + + g_task_return_error (task, err); + return; + } + + handle = polkit_agent_listener_register (POLKIT_AGENT_LISTENER (self), + POLKIT_AGENT_REGISTER_FLAGS_NONE, + subject, + NULL, /* use default object path */ + cancellable, + &err); + + if (handle) + g_task_return_pointer (task, handle, polkit_agent_listener_unregister); + else + g_task_return_error (task, err); + +} + +static gpointer +agent_register_finish (PhoshPolkitAuthAgent *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_pointer (G_TASK (res), error); +} + +static void +agent_register_async (PhoshPolkitAuthAgent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr (GTask) task = g_task_new (self, cancellable, callback, user_data); + + g_task_run_in_thread (task, agent_register_thread); +} + +static void +on_agent_registered (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (source_object); + g_autoptr (GError) err = NULL; + + self->handle = agent_register_finish (self, res, &err); + + g_clear_object (&self->cancellable); + + if (!self->handle) { + g_warning ("Auth agent failed to register: %s", err->message); + return; + } + + g_debug ("Polkit auth agent registered"); +} + +static void +auth_request_free (AuthRequest *request) +{ + g_free (request->action_id); + g_free (request->message); + g_free (request->icon_name); + g_object_unref (request->details); + g_free (request->cookie); + g_list_foreach (request->identities, (GFunc) g_object_unref, NULL); + g_list_free (request->identities); + g_object_unref (request->simple); + g_free (request); +} + + +static void +close_prompt (PhoshPolkitAuthAgent *self) +{ + g_clear_pointer ((PhoshSystemModalDialog**)&self->current_prompt, + phosh_system_modal_dialog_close); +} + + +static void +on_prompt_done (PhoshPolkitAuthPrompt *prompt, gboolean cancelled, AuthRequest *request) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (prompt)); + + close_prompt (request->agent); + auth_request_complete (request, cancelled); +} + + +static void +auth_request_initiate (AuthRequest *request) +{ + g_auto (GStrv) user_names = NULL; + GPtrArray *p; + GList *l; + + p = g_ptr_array_new (); + for (l = request->identities; l != NULL; l = l->next) { + if (POLKIT_IS_UNIX_USER (l->data)) { + PolkitUnixUser *user = POLKIT_UNIX_USER (l->data); + int uid; + char buf[4096]; + struct passwd pwd; + struct passwd *ppwd; + int ret; + + uid = polkit_unix_user_get_uid (user); + ret = getpwuid_r (uid, &pwd, buf, sizeof (buf), &ppwd); + if (!ret) { + if (!g_utf8_validate (pwd.pw_name, -1, NULL)) + g_warning ("Invalid UTF-8 in username for uid %d. Skipping", uid); + else + g_ptr_array_add (p, g_strdup (pwd.pw_name)); + } else { + g_warning ("Error looking up user name for uid %d: %d", uid, ret); + } + } else { + g_warning ("Unsupporting identity of GType %s", g_type_name (G_TYPE_FROM_INSTANCE (l->data))); + } + } + + g_ptr_array_add (p, NULL); + user_names = (char **) g_ptr_array_free (p, FALSE); + + g_debug ("New prompt for %s", request->message); + /* We must not issue a new prompt when there's one already */ + g_return_if_fail (!request->agent->current_prompt); + request->agent->current_prompt = PHOSH_POLKIT_AUTH_PROMPT ( + phosh_polkit_auth_prompt_new ( + request->action_id, + request->message, + request->icon_name, + request->cookie, + user_names)); + + g_signal_connect (request->agent->current_prompt, + "done", + G_CALLBACK (on_prompt_done), + request); + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + request->agent->current_prompt, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); +} + +static void +maybe_process_next_request (PhoshPolkitAuthAgent *self) +{ + auth_debug ("cur=%p len(scheduled)=%d", + self->current_request, + g_list_length (self->scheduled_requests)); + + if (self->current_request == NULL && self->scheduled_requests != NULL) { + AuthRequest *request; + + request = self->scheduled_requests->data; + + self->current_request = request; + self->scheduled_requests = g_list_remove (self->scheduled_requests, request); + + auth_debug ("initiating %s cookie %s", request->action_id, request->cookie); + auth_request_initiate (request); + } +} + + +static void +auth_request_complete (AuthRequest *request, gboolean dismissed) +{ + PhoshPolkitAuthAgent *self = request->agent; + gboolean is_current = self->current_request == request; + + auth_debug ("completing %s %s cookie %s", is_current ? "current" : "scheduled", + request->action_id, request->cookie); + + if (!is_current) + self->scheduled_requests = g_list_remove (self->scheduled_requests, request); + g_cancellable_disconnect (request->cancellable, request->handler_id); + + if (dismissed) { + g_task_return_new_error (request->simple, + POLKIT_ERROR, + POLKIT_ERROR_CANCELLED, + _("Authentication dialog was dismissed by the user")); + } else { + g_task_return_boolean (request->simple, TRUE); + } + + auth_request_free (request); + + if (is_current) { + self->current_request = NULL; + maybe_process_next_request (self); + } +} + +static gboolean +handle_cancelled_in_idle (gpointer user_data) +{ + AuthRequest *request = user_data; + + auth_debug ("cancelled %s cookie %s", request->action_id, request->cookie); + if (request == request->agent->current_request) + close_prompt (request->agent); + + auth_request_complete (request, FALSE); + return FALSE; +} + +/* + * on_request_cancelled: + * + * This happens when the application requesting authentication + * is closed. + */ +static void +on_request_cancelled (GCancellable *cancellable, + gpointer user_data) +{ + AuthRequest *request = user_data; + guint id; + + /* + * post-pone to idle to handle GCancellable deadlock in + * https://bugzilla.gnome.org/show_bug.cgi?id=642968 + * https://gitlab.gnome.org/GNOME/glib/issues/740 + */ + id = g_idle_add (handle_cancelled_in_idle, request); + g_source_set_name_by_id (id, "[phosh] handle_cancelled_in_idle"); +} + + +static void +initiate_authentication (PolkitAgentListener *listener, + const char *action_id, + const char *message, + const char *icon_name, + PolkitDetails *details, + const char *cookie, + GList *identities, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (listener); + AuthRequest *request; + + request = g_new0 (AuthRequest, 1); + request->agent = self; + request->action_id = g_strdup (action_id); + request->message = g_strdup (message); + request->icon_name = g_strdup (icon_name); + request->details = g_object_ref (details); + request->cookie = g_strdup (cookie); + request->identities = g_list_copy (identities); + g_list_foreach (request->identities, (GFunc) g_object_ref, NULL); + request->simple = g_task_new (listener, NULL, callback, user_data); + request->cancellable = cancellable; + request->handler_id = g_cancellable_connect (request->cancellable, + G_CALLBACK (on_request_cancelled), + request, + NULL); /* GDestroyNotify for request */ + + auth_debug ("scheduling %s cookie %s", request->action_id, request->cookie); + self->scheduled_requests = g_list_append (self->scheduled_requests, request); + + maybe_process_next_request (self); +} + +static gboolean +initiate_authentication_finish (PolkitAgentListener *listener, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + + +static void +phosh_polkit_auth_agent_constructed (GObject *object) +{ + PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (object); + + G_OBJECT_CLASS (phosh_polkit_auth_agent_parent_class)->constructed (object); + + self->cancellable = g_cancellable_new (); + agent_register_async (self, self->cancellable, on_agent_registered, NULL); +} + + +static void +auth_request_dismiss (AuthRequest *request) +{ + auth_request_complete (request, TRUE); +} + + +static void +phosh_polkit_auth_agent_unregister (PhoshPolkitAuthAgent *self) +{ + if (self->scheduled_requests != NULL) { + g_list_foreach (self->scheduled_requests, (GFunc)auth_request_dismiss, NULL); + self->scheduled_requests = NULL; + } + + if (self->current_request != NULL) + auth_request_dismiss (self->current_request); + + polkit_agent_listener_unregister (self->handle); + self->handle = NULL; +} + + +static void +phosh_polkit_auth_agent_dispose (GObject *object) +{ + PhoshPolkitAuthAgent *self = PHOSH_POLKIT_AUTH_AGENT (object); + + if (self->handle) + phosh_polkit_auth_agent_unregister (self); + + g_cancellable_cancel (self->cancellable); + g_clear_object (&self->cancellable); + + G_OBJECT_CLASS (phosh_polkit_auth_agent_parent_class)->dispose (object); +} + + +static void +phosh_polkit_auth_agent_class_init (PhoshPolkitAuthAgentClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PolkitAgentListenerClass *listener_class; + + object_class->constructed = phosh_polkit_auth_agent_constructed; + object_class->dispose = phosh_polkit_auth_agent_dispose; + + listener_class = POLKIT_AGENT_LISTENER_CLASS (klass); + listener_class->initiate_authentication = initiate_authentication; + listener_class->initiate_authentication_finish = initiate_authentication_finish; +} + + +static void +phosh_polkit_auth_agent_init (PhoshPolkitAuthAgent *self) +{ +} + + +PhoshPolkitAuthAgent * +phosh_polkit_auth_agent_new (void) +{ + return g_object_new (PHOSH_TYPE_POLKIT_AUTH_AGENT, NULL); +} diff --git a/src/polkit-auth-agent.h b/src/polkit-auth-agent.h new file mode 100644 index 000000000..788af9a70 --- /dev/null +++ b/src/polkit-auth-agent.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_POLKIT_AUTH_AGENT phosh_polkit_auth_agent_get_type () + +G_DECLARE_FINAL_TYPE (PhoshPolkitAuthAgent, phosh_polkit_auth_agent, PHOSH, POLKIT_AUTH_AGENT, + PolkitAgentListener) + +PhoshPolkitAuthAgent * phosh_polkit_auth_agent_new (void); +void phosh_polkit_authentication_agent_register (PhoshPolkitAuthAgent *agent, + GError **error); + +G_END_DECLS diff --git a/src/polkit-auth-prompt.c b/src/polkit-auth-prompt.c new file mode 100644 index 000000000..45a497c5a --- /dev/null +++ b/src/polkit-auth-prompt.c @@ -0,0 +1,657 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Based on gnome-shell's shell-keyring-prompt.c + * Author: Stef Walter + */ + +#define G_LOG_DOMAIN "phosh-polkit-auth-prompt" + +#include "phosh-config.h" + +#include "accounts-dbus.h" +#include "accounts-user-dbus.h" +#include "polkit-auth-prompt.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include + +#define POLKIT_AGENT_I_KNOW_API_IS_SUBJECT_TO_CHANGE +#include + +#include +#include + +/** + * PhoshPolkitAuthPrompt: + * + * A modal prompt for policy kit authentication + * + * The #PhoshPolkitAuthPrompt is used to ask policy kit authentication. + * This handles the interaction with PolkitAgentSession. + */ + +enum { + PROP_0, + PROP_ACTION_ID, + PROP_COOKIE, + PROP_MESSAGE, + PROP_ICON_NAME, + PROP_USER_NAMES, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +struct _PhoshPolkitAuthPrompt { + PhoshSystemModalDialog parent; + + GtkWidget *lbl_message; + GtkWidget *lbl_user_name; + GtkWidget *lbl_password; + GtkWidget *lbl_info; + GtkWidget *img_icon; + GtkWidget *btn_authenticate; + GtkWidget *spinner_authenticate; + GtkWidget *btn_cancel; + GtkWidget *entry_password; + GtkEntryBuffer *password_buffer; + + char *action_id; + char *message; + char *icon_name; + char *cookie; + GStrv user_names; + + const char *user_name; + GCancellable *cancel; + + PolkitIdentity *identity; + PolkitAgentSession *session; + + gboolean done_emitted; +}; + +G_DEFINE_TYPE (PhoshPolkitAuthPrompt, phosh_polkit_auth_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + + +static void phosh_polkit_auth_prompt_initiate (PhoshPolkitAuthPrompt *self); + + +static void +set_action_id (PhoshPolkitAuthPrompt *self, const char *action_id) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + if (!g_strcmp0 (self->action_id, action_id)) + return; + + g_clear_pointer (&self->action_id, g_free); + self->action_id = g_strdup (action_id); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTION_ID]); +} + + +static void +set_cookie (PhoshPolkitAuthPrompt *self, const char *cookie) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + if (!g_strcmp0 (self->cookie, cookie)) + return; + + g_clear_pointer (&self->cookie, g_free); + self->cookie = g_strdup (cookie); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_COOKIE]); +} + + +static void +set_message (PhoshPolkitAuthPrompt *self, const char *message) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + if (!g_strcmp0 (self->message, message)) + return; + + g_clear_pointer (&self->message, g_free); + self->message = g_strdup (message); + gtk_label_set_label (GTK_LABEL (self->lbl_message), message ?: ""); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MESSAGE]); +} + + +static void +set_icon_name (PhoshPolkitAuthPrompt *self, const char *icon_name) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + if (!g_strcmp0 (self->icon_name, icon_name)) + return; + + g_clear_pointer (&self->icon_name, g_free); + self->icon_name = g_strdup (icon_name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +static void +on_accounts_user_proxy_ready (GObject *source, GAsyncResult *result, gpointer data) +{ + PhoshPolkitAuthPrompt *self = data; + g_autoptr (GError) error = NULL; + g_autoptr (PhoshDBusAccountsUser) user = NULL; + const char *real_name = NULL; + const char *icon_path = NULL; + g_autoptr (GFile) icon_file = NULL; + g_autoptr (GIcon) icon = NULL; + + user = phosh_dbus_accounts_user_proxy_new_finish (result, &error); + if (error) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Skipping create user proxy: %s", error->message); + else + g_warning ("Failed to create user proxy %s: %s", self->user_name, error->message); + return; + } + g_debug ("Created proxy for user name: %s", self->user_name); + + real_name = phosh_dbus_accounts_user_get_real_name (user); + hdy_avatar_set_text (HDY_AVATAR (self->img_icon), real_name); + gtk_label_set_text (GTK_LABEL (self->lbl_user_name), real_name); + + icon_path = phosh_dbus_accounts_user_get_icon_file (user); + icon_file = g_file_new_for_path (icon_path); + icon = g_file_icon_new (icon_file); + hdy_avatar_set_loadable_icon (HDY_AVATAR (self->img_icon), G_LOADABLE_ICON (icon)); +} + + +static void +on_find_user_by_name_ready (GObject *source, GAsyncResult *result, gpointer data) +{ + g_autoptr (PhoshDBusAccounts) accounts = PHOSH_DBUS_ACCOUNTS (source); + PhoshPolkitAuthPrompt *self = data; + g_autoptr (GError) error = NULL; + g_autofree char *user_path = NULL; + + phosh_dbus_accounts_call_find_user_by_name_finish (accounts, &user_path, result, &error); + if (error) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Skipping find user: %s", error->message); + else + g_warning ("Failed to find user %s: %s", self->user_name, error->message); + return; + } + g_debug ("Found user for user name: %s at %s", self->user_name, user_path); + + g_debug ("Creating proxy for user name: %s", self->user_name); + phosh_dbus_accounts_user_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS, + "org.freedesktop.Accounts", + user_path, + self->cancel, + on_accounts_user_proxy_ready, + self); +} + + +static void +on_accounts_proxy_ready (GObject *source, GAsyncResult *result, gpointer data) +{ + PhoshDBusAccounts *accounts; + PhoshPolkitAuthPrompt *self = data; + g_autoptr (GError) error = NULL; + + accounts = phosh_dbus_accounts_proxy_new_finish (result, &error); + if (error) { + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Skipping create Accounts proxy: %s", error->message); + else + g_warning ("Failed to create Accounts proxy: %s", error->message); + return; + } + g_debug ("Created Accounts proxy for user name: %s", self->user_name); + + g_debug ("Finding user for user name: %s", self->user_name); + phosh_dbus_accounts_call_find_user_by_name (accounts, + self->user_name, + self->cancel, + on_find_user_by_name_ready, + self); +} + + +static void +set_user_names (PhoshPolkitAuthPrompt *self, const GStrv user_names) +{ + const char *user_name = NULL; + GError *err = NULL; + int len; + + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + /* if both are NULL */ + if (self->user_names == user_names) + return; + + g_strfreev (self->user_names); + self->user_names = g_strdupv (user_names); + + len = g_strv_length (self->user_names); + if (len == 0) { + user_name = "unknown"; + } else if (len == 1) { + user_name = self->user_names[0]; + } else { + g_debug ("Received %d user names, only using one", len); + user_name = g_get_user_name (); + + if (!g_strv_contains ((const char *const *)user_names, user_name)) + user_name = "root"; + + if (!g_strv_contains ((const char *const *)user_names, user_name)) + user_name = user_names[0]; + } + + self->identity = polkit_unix_user_new_for_name (user_name, &err); + if (!self->identity) { + g_warning ("Failed to create identity: %s", err->message); + g_clear_error (&err); + return; + } + + hdy_avatar_set_text (HDY_AVATAR (self->img_icon), user_name); + gtk_label_set_text (GTK_LABEL (self->lbl_user_name), user_name); + + self->user_name = user_name; + g_debug ("Creating Accounts proxy for user name: %s", self->user_name); + phosh_dbus_accounts_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + (G_DBUS_PROXY_FLAGS_DO_NOT_LOAD_PROPERTIES | + G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS), + "org.freedesktop.Accounts", + "/org/freedesktop/Accounts", + self->cancel, + on_accounts_proxy_ready, + self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USER_NAMES]); +} + + +static void +phosh_polkit_auth_prompt_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshPolkitAuthPrompt *self = PHOSH_POLKIT_AUTH_PROMPT (object); + + switch (property_id) { + case PROP_ACTION_ID: + set_action_id (self, g_value_get_string (value)); + break; + case PROP_COOKIE: + set_cookie (self, g_value_get_string (value)); + break; + case PROP_MESSAGE: + set_message (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + set_icon_name (self, g_value_get_string (value)); + break; + case PROP_USER_NAMES: + set_user_names (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_polkit_auth_prompt_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshPolkitAuthPrompt *self = PHOSH_POLKIT_AUTH_PROMPT (object); + + switch (property_id) { + case PROP_ACTION_ID: + g_value_set_string (value, self->action_id ?: ""); + break; + case PROP_COOKIE: + g_value_set_string (value, self->cookie ?: ""); + break; + case PROP_MESSAGE: + g_value_set_string (value, self->message ?: ""); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name ?: ""); + break; + case PROP_USER_NAMES: + g_value_set_boxed (value, self->user_names ? g_strdupv (self->user_names) : NULL); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +emit_done (PhoshPolkitAuthPrompt *self, gboolean cancelled) +{ + g_debug ("Emitting done. Cancelled: %d", cancelled); + + if (!self->done_emitted) { + self->done_emitted = TRUE; + g_signal_emit (self, signals[DONE], 0 /* detail */, cancelled); + } +} + + +static void +on_auth_session_request (PhoshPolkitAuthPrompt *self, + char *request, + gboolean echo_on, + gpointer user_data) +{ + g_debug ("Request: %s, echo: %d", request, echo_on); + + if (self->done_emitted) + return; + + /* Translate the most common request */ + if (!g_strcmp0 (request, "Password: ") || !g_strcmp0 (request, "Password:")) + request = _("Password:"); + + gtk_label_set_text (GTK_LABEL (self->lbl_password), request); + gtk_entry_set_visibility (GTK_ENTRY (self->entry_password), echo_on); + gtk_entry_set_text (GTK_ENTRY (self->entry_password), ""); + gtk_widget_grab_focus (self->entry_password); +} + +static void +on_auth_session_show_error (PhoshPolkitAuthPrompt *self, + char *text, + PolkitAgentSession *session) +{ + g_debug ("%s", text); + gtk_label_set_text (GTK_LABEL (self->lbl_info), text); +} + + +static void +on_auth_session_show_info (PhoshPolkitAuthPrompt *self, + char *text, + PolkitAgentSession *session) +{ + g_debug ("%s", text); + gtk_label_set_text (GTK_LABEL (self->lbl_info), text); +} + + +static void +on_auth_session_completed (PhoshPolkitAuthPrompt *self, + gboolean gained_auth, + PolkitAgentSession *session) +{ + g_debug ("Gained auth: %d", gained_auth); + gtk_spinner_stop (GTK_SPINNER (self->spinner_authenticate)); + gtk_widget_set_visible (self->spinner_authenticate, FALSE); + + if (self->done_emitted) + return; + + if (gained_auth) { + emit_done (self, FALSE); + } else { + const char *info = gtk_label_get_text (GTK_LABEL (self->lbl_info)); + + if (info == NULL || !g_strcmp0 (info, "")) { + gtk_label_set_text (GTK_LABEL (self->lbl_info), + _("Sorry, that didn’t work. Please try again.")); + } + /* Start new auth session */ + g_signal_handlers_disconnect_by_data (session, self); + phosh_polkit_auth_prompt_initiate (self); + } +} + + +static void +phosh_polkit_auth_prompt_initiate (PhoshPolkitAuthPrompt *self) +{ + g_return_if_fail (self->identity); + g_return_if_fail (self->cookie); + + self->session = polkit_agent_session_new (self->identity, + self->cookie); + + g_signal_connect_swapped (self->session, + "request", + G_CALLBACK (on_auth_session_request), + self); + + g_signal_connect_swapped (self->session, + "show-error", + G_CALLBACK (on_auth_session_show_error), + self); + + g_signal_connect_swapped (self->session, + "show-info", + G_CALLBACK (on_auth_session_show_info), + self); + + g_signal_connect_swapped (self->session, + "completed", + G_CALLBACK (on_auth_session_completed), + self); + + polkit_agent_session_initiate (self->session); +} + + +static void +on_dialog_canceled (PhoshPolkitAuthPrompt *self, gpointer unused) +{ + g_return_if_fail (PHOSH_IS_POLKIT_AUTH_PROMPT (self)); + + polkit_agent_session_cancel (self->session); + emit_done (self, TRUE); +} + + +static void +on_btn_authenticate_clicked (PhoshPolkitAuthPrompt *self, GtkButton *btn) +{ + const char *password; + + password = gtk_entry_buffer_get_text (self->password_buffer); + + if (!password || strlen (password) == 0) + return; + + gtk_label_set_text (GTK_LABEL (self->lbl_info), ""); + gtk_widget_set_visible (self->spinner_authenticate, TRUE); + gtk_spinner_start (GTK_SPINNER (self->spinner_authenticate)); + polkit_agent_session_response (self->session, password); +} + + +static void +phosh_polkit_auth_prompt_dispose (GObject *object) +{ + PhoshPolkitAuthPrompt *self = PHOSH_POLKIT_AUTH_PROMPT (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + self->user_name = NULL; + g_clear_object (&self->identity); + g_clear_object (&self->session); + + G_OBJECT_CLASS (phosh_polkit_auth_prompt_parent_class)->dispose (object); +} + + +static void +phosh_polkit_auth_prompt_finalize (GObject *object) +{ + PhoshPolkitAuthPrompt *self = PHOSH_POLKIT_AUTH_PROMPT (object); + + g_free (self->message); + g_free (self->icon_name); + g_free (self->action_id); + g_free (self->cookie); + g_strfreev (self->user_names); + + G_OBJECT_CLASS (phosh_polkit_auth_prompt_parent_class)->finalize (object); +} + + +static void +phosh_polkit_auth_prompt_constructed (GObject *object) +{ + PhoshPolkitAuthPrompt *self = PHOSH_POLKIT_AUTH_PROMPT (object); + + G_OBJECT_CLASS (phosh_polkit_auth_prompt_parent_class)->constructed (object); + + self->cancel = g_cancellable_new (); + + self->password_buffer = gcr_secure_entry_buffer_new (); + gtk_entry_set_buffer (GTK_ENTRY (self->entry_password), + GTK_ENTRY_BUFFER (self->password_buffer)); + + phosh_polkit_auth_prompt_initiate (self); +} + + +static void +phosh_polkit_auth_prompt_class_init (PhoshPolkitAuthPromptClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_polkit_auth_prompt_get_property; + object_class->set_property = phosh_polkit_auth_prompt_set_property; + object_class->constructed = phosh_polkit_auth_prompt_constructed; + object_class->dispose = phosh_polkit_auth_prompt_dispose; + object_class->finalize = phosh_polkit_auth_prompt_finalize; + + /** + * PolkitAuthPrompt:action-id: + * + * The prompt's action id + */ + props[PROP_ACTION_ID] = + g_param_spec_string ("action-id", "", "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + /** + * PolkitAuthPrompt:cookie: + * + * The prompt's cookie + */ + props[PROP_COOKIE] = + g_param_spec_string ("cookie", "", "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + /** + * PolkitAuthPrompt:message: + * + * The prompt's message + */ + props[PROP_MESSAGE] = + g_param_spec_string ("message", "", "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + /** + * PolkitAuthPrompt:icon-name: + * + * The prompt's icon name + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + "", + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + /** + * PolkitAuthPrompt:user-names: + * + * The user names to authenticate as + */ + props[PROP_USER_NAMES] = + g_param_spec_boxed ("user-names", "", "", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshPolkitAuthPrompt::done: + * @self: The #PhoshPolkitAuthPrompt instance. + * @cancelled: Whether the dialog was cancelled + * + * This signal is emitted when the prompt can be closed. The cancelled + * argument indicates whether the prompt was cancelled. + */ + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, G_TYPE_BOOLEAN); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/polkit-auth-prompt.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, btn_authenticate); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, btn_cancel); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, entry_password); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, img_icon); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, lbl_info); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, lbl_password); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, lbl_user_name); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, spinner_authenticate); + gtk_widget_class_bind_template_child (widget_class, PhoshPolkitAuthPrompt, lbl_message); + + gtk_widget_class_bind_template_callback (widget_class, on_btn_authenticate_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); +} + + +static void +phosh_polkit_auth_prompt_init (PhoshPolkitAuthPrompt *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_polkit_auth_prompt_new (const char *action_id, + const char *message, + const char *icon_name, + const char *cookie, + GStrv user_names) +{ + return g_object_new (PHOSH_TYPE_POLKIT_AUTH_PROMPT, + "action-id", action_id, + "cookie", cookie, + "message", message, + "icon-name", icon_name, + "user-names", user_names, + NULL); +} diff --git a/src/polkit-auth-prompt.h b/src/polkit-auth-prompt.h new file mode 100644 index 000000000..ab0d6d6e2 --- /dev/null +++ b/src/polkit-auth-prompt.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_POLKIT_AUTH_PROMPT phosh_polkit_auth_prompt_get_type () + +G_DECLARE_FINAL_TYPE (PhoshPolkitAuthPrompt, phosh_polkit_auth_prompt, PHOSH, POLKIT_AUTH_PROMPT, + PhoshSystemModalDialog); + +GtkWidget *phosh_polkit_auth_prompt_new (const char *action_id, + const char *message, + const char *icon_name, + const char *cookie, + GStrv user_names); + +G_END_DECLS diff --git a/src/portal-access-manager.c b/src/portal-access-manager.c new file mode 100644 index 000000000..9b935cc8b --- /dev/null +++ b/src/portal-access-manager.c @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#define G_LOG_DOMAIN "phosh-portal-access-manager" + +#include "app-auth-prompt.h" +#include "dbus/portal-dbus.h" +#include "portal-access-manager.h" +#include "portal-request.h" +#include "shell-priv.h" +#include "util.h" + +#define PORTAL_DBUS_NAME "mobi.phosh.Shell.Portal" +#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop" +#define PORTAL_ACCESS_DIALOG_GRANTED 0 +#define PORTAL_ACCESS_DIALOG_DENIED 1 +#define PORTAL_ACCESS_DIALOG_ALREADY_OPEN 2 + + +/** + * PhoshPortalAccessManager: + * + * Implements org.freedesktop.impl.portal + */ + +typedef struct _PhoshPortalAccessManager { + PhoshDBusImplPortalAccessSkeleton access_portal; + PhoshPortalRequest *request; + int dbus_name_id; + PhoshAppAuthPrompt *app_auth_prompt; + GDBusMethodInvocation *invocation; + GVariant *choices; +} PhoshPortalAccessManager; + +static void +phosh_portal_access_manager_access_iface_init (PhoshDBusImplPortalAccessIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshPortalAccessManager, + phosh_portal_access_manager, + PHOSH_DBUS_TYPE_IMPL_PORTAL_ACCESS_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_IMPL_PORTAL_ACCESS, + phosh_portal_access_manager_access_iface_init)); + +static void +on_access_dialog_closed (PhoshPortalAccessManager *self) +{ + g_autoptr (GVariantDict) results = g_variant_dict_new (NULL); + GVariant *choices = NULL; + gboolean granted = phosh_app_auth_prompt_get_grant_access (GTK_WIDGET (self->app_auth_prompt)); + guint response = PORTAL_ACCESS_DIALOG_DENIED; + + g_clear_object (&self->request); + + if (granted) + response = PORTAL_ACCESS_DIALOG_GRANTED; + + choices = phosh_app_auth_prompt_get_selected_choices (GTK_WIDGET (self->app_auth_prompt)); + g_variant_dict_insert_value (results, "choices", choices); + phosh_dbus_impl_portal_access_complete_access_dialog (PHOSH_DBUS_IMPL_PORTAL_ACCESS (self), + self->invocation, + response, + g_variant_dict_end (results)); + self->invocation = NULL; + + if (self->app_auth_prompt != NULL) + gtk_widget_set_visible (GTK_WIDGET (self->app_auth_prompt), FALSE); + g_clear_pointer (&self->app_auth_prompt, phosh_cp_widget_destroy); +} + + +static gboolean +handle_access_dialog (PhoshDBusImplPortalAccess *object, + GDBusMethodInvocation *invocation, + const char *arg_handle, + const char *arg_app_id, + const char *arg_parent_window, + const char *arg_title, + const char *arg_subtitle, + const char *arg_body, + GVariant *arg_options) +{ + const char *sender; + const char *grant_label = NULL; + const char *deny_label = NULL; + g_autoptr (GIcon) icon = NULL; + GVariant *choices = NULL; + GVariant *icon_variant = g_variant_lookup_value (arg_options, "icon", G_VARIANT_TYPE_STRING); + PhoshPortalAccessManager *self = PHOSH_PORTAL_ACCESS_MANAGER (object); + g_autoptr (PhoshPortalRequest) request = NULL; + + sender = g_dbus_method_invocation_get_sender (invocation); + request = phosh_portal_request_new (sender, arg_app_id, arg_handle); + + if (self->app_auth_prompt != NULL) { + return FALSE; + } + if (icon_variant != NULL) { + const char *icon_name_str = g_variant_get_string (icon_variant, NULL); + icon = g_themed_icon_new (icon_name_str); + } + g_variant_lookup (arg_options, "grant_label", "&s", grant_label); + g_variant_lookup (arg_options, "deny_label", "&s", deny_label); + choices = g_variant_lookup_value (arg_options, "choices", + G_VARIANT_TYPE (PHOSH_APP_AUTH_PROMPT_CHOICES_FORMAT)); + + self->invocation = invocation; + self->choices = choices; + self->request = g_steal_pointer (&request); + self->app_auth_prompt = PHOSH_APP_AUTH_PROMPT ( + phosh_app_auth_prompt_new (icon, + arg_title, + arg_subtitle, + arg_body, + grant_label, + deny_label, + FALSE, + choices)); + + g_signal_connect_object (self->app_auth_prompt, + "closed", + G_CALLBACK (on_access_dialog_closed), + self, + G_CONNECT_SWAPPED); + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + self->app_auth_prompt, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + phosh_portal_request_export (self->request, g_dbus_method_invocation_get_connection (invocation)); + return TRUE; +} + +static void +phosh_portal_access_manager_access_iface_init (PhoshDBusImplPortalAccessIface *iface) +{ + iface->handle_access_dialog = handle_access_dialog; +} + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Acquired name %s", name); +} + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshPortalAccessManager *self = user_data; + g_autoptr (GError) err = NULL; + + if (g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + PORTAL_OBJECT_PATH, + &err)) { + g_debug ("Access portal exported"); + } else { + g_warning ("Failed to export on %s: %s", PORTAL_DBUS_NAME, err->message); + } +} + +static void +phosh_portal_access_manager_constructed (GObject *object) +{ + PhoshPortalAccessManager *self = PHOSH_PORTAL_ACCESS_MANAGER (object); + + G_OBJECT_CLASS (phosh_portal_access_manager_parent_class)->constructed (object); + + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + PORTAL_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); +} + +static void +phosh_portal_access_manager_dispose (GObject *object) +{ + PhoshPortalAccessManager *self = PHOSH_PORTAL_ACCESS_MANAGER (object); + + g_clear_pointer (&self->app_auth_prompt, phosh_cp_widget_destroy); + g_clear_object (&self->request); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + G_OBJECT_CLASS (phosh_portal_access_manager_parent_class)->dispose (object); +} + +static void +phosh_portal_access_manager_class_init (PhoshPortalAccessManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_portal_access_manager_constructed; + object_class->dispose = phosh_portal_access_manager_dispose; +} + +static void +phosh_portal_access_manager_init (PhoshPortalAccessManager *self) +{ +} + +PhoshPortalAccessManager * +phosh_portal_access_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_PORTAL_ACCESS_MANAGER, NULL); +} diff --git a/src/portal-access-manager.h b/src/portal-access-manager.h new file mode 100644 index 000000000..f8d46d583 --- /dev/null +++ b/src/portal-access-manager.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#pragma once + +#include "dbus/portal-dbus.h" + +#define PHOSH_TYPE_PORTAL_ACCESS_MANAGER (phosh_portal_access_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshPortalAccessManager, phosh_portal_access_manager, + PHOSH, PORTAL_ACCESS_MANAGER, + PhoshDBusImplPortalAccessSkeleton); + +PhoshPortalAccessManager *phosh_portal_access_manager_new (void); diff --git a/src/portal-request.c b/src/portal-request.c new file mode 100644 index 000000000..7c30467fb --- /dev/null +++ b/src/portal-request.c @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#include "portal-request.h" +#include "dbus/portal-dbus.h" + +/** + * PhoshPortalRequest: + * + * Shared request api for all portal backend interfaces. + * + * The PhoshPortalRequest implements the Request backend interface. + * See the portal documentation: https://flatpak.github.io/xdg-desktop-portal/#gdbus-org.freedesktop.impl.portal.Request. + * + * The request object will alive as long as it is exported. Users of the request must ensure to export/unexport properly. + */ + +static void +phosh_portal_request_iface_init (PhoshDBusImplPortalRequestIface *iface); + +typedef struct _PhoshPortalRequest { + PhoshDBusImplPortalRequestSkeleton parent_instance; + + gboolean exported; + char *id; + char *sender; + char *app_id; + +} PhoshPortalRequest; + +G_DEFINE_TYPE_WITH_CODE (PhoshPortalRequest, + phosh_portal_request, + PHOSH_DBUS_TYPE_IMPL_PORTAL_REQUEST_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_IMPL_PORTAL_REQUEST, + phosh_portal_request_iface_init)); + +static gboolean +handle_close (PhoshDBusImplPortalRequest *object, GDBusMethodInvocation *invocation) +{ + PhoshPortalRequest *self = (PhoshPortalRequest *)object; + + if (self->exported) + phosh_portal_request_unexport (self); + + phosh_dbus_impl_portal_request_complete_close (object, invocation); + + return TRUE; +} + +static void +phosh_portal_request_iface_init (PhoshDBusImplPortalRequestIface *iface) +{ + iface->handle_close = handle_close; +} + +static void +phosh_portal_request_finalize (GObject *object) +{ + PhoshPortalRequest *self = PHOSH_PORTAL_REQUEST (object); + + phosh_portal_request_unexport (self); + g_free (self->sender); + g_free (self->app_id); + g_free (self->id); + + G_OBJECT_CLASS (phosh_portal_request_parent_class)->finalize (object); +} + +static void +phosh_portal_request_class_init (PhoshPortalRequestClass *klass) +{ + GObjectClass *gobject_class; + + gobject_class = G_OBJECT_CLASS (klass); + gobject_class->finalize = phosh_portal_request_finalize; +} + +static void +phosh_portal_request_init (PhoshPortalRequest *request) +{ +} + +PhoshPortalRequest * +phosh_portal_request_new (const char *sender, const char *app_id, const char *id) +{ + PhoshPortalRequest *request; + + request = g_object_new (PHOSH_TYPE_PORTAL_REQUEST, NULL); + request->sender = g_strdup (sender); + request->app_id = g_strdup (app_id); + request->id = g_strdup (id); + + return request; +} + +gboolean +phosh_portal_request_exported (PhoshPortalRequest *self) +{ + g_return_val_if_fail (PHOSH_IS_PORTAL_REQUEST (self), FALSE); + + return self->exported; +} + +void +phosh_portal_request_export (PhoshPortalRequest *self, GDBusConnection *connection) +{ + g_autoptr (GError) error = NULL; + + g_return_if_fail (PHOSH_IS_PORTAL_REQUEST (self)); + g_return_if_fail (G_IS_DBUS_CONNECTION (connection)); + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, self->id, + &error)) { + g_warning ("Error exporting request: %s", error->message); + self->exported = TRUE; + } +} + +void +phosh_portal_request_unexport (PhoshPortalRequest *self) +{ + g_return_if_fail (PHOSH_IS_PORTAL_REQUEST (self)); + + if (self->exported) { + self->exported = FALSE; + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + } +} diff --git a/src/portal-request.h b/src/portal-request.h new file mode 100644 index 000000000..7ff1b5bb6 --- /dev/null +++ b/src/portal-request.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#pragma once + +#include "dbus/portal-dbus.h" + +#define PHOSH_TYPE_PORTAL_REQUEST (phosh_portal_request_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshPortalRequest, phosh_portal_request, + PHOSH, PORTAL_REQUEST, + PhoshDBusImplPortalRequestSkeleton); + +PhoshPortalRequest *phosh_portal_request_new (const char *sender, const char *app_id, const char *id); +gboolean phosh_portal_request_exported (PhoshPortalRequest *self); +void phosh_portal_request_export (PhoshPortalRequest *self, GDBusConnection *connection); +void phosh_portal_request_unexport (PhoshPortalRequest *self); diff --git a/src/power-menu-manager.c b/src/power-menu-manager.c new file mode 100644 index 000000000..ef81e962b --- /dev/null +++ b/src/power-menu-manager.c @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-power-menu-manager" + +#include "power-menu-manager.h" +#include "power-menu.h" +#include "shell-priv.h" +#include "util.h" + +/** + * PhoshPowerMenuManager: + * + * Handles the power button menu + * + * The interface is responsible to handle the non-ui parts of a + * #PhoshPowerDialog. + */ + +typedef struct _PhoshPowerMenuManager { + GObject parent; + + PhoshPowerMenu *dialog; + + GSimpleActionGroup *menu_actions; +} PhoshPowerMenuManager; + +G_DEFINE_TYPE (PhoshPowerMenuManager, phosh_power_menu_manager, G_TYPE_OBJECT) + + +static void +close_menu (PhoshPowerMenuManager *self) +{ + g_clear_pointer (&self->dialog, phosh_cp_widget_destroy); +} + + +static void +on_power_menu_done (PhoshPowerMenuManager *self) +{ + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + + close_menu (self); +} + + +static void +on_power_off_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + + close_menu (self); + phosh_session_manager_shutdown (sm); +} + + +static void +on_suspend_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + + close_menu (self); + g_action_group_activate_action (G_ACTION_GROUP (phosh_shell_get_default ()), + "suspend.trigger-suspend", NULL); +} + + +static void +on_screen_lock_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + PhoshShell *shell = phosh_shell_get_default (); + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + + close_menu (self); + phosh_shell_set_locked (shell, TRUE); +} + + +static void +on_screenshot_activated (GSimpleAction *action, GVariant *param, gpointer data) + +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + PhoshScreenshotManager *manager = + phosh_shell_get_screenshot_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (manager)); + + close_menu (self); + phosh_screenshot_manager_take_screenshot (manager, NULL, NULL, TRUE, FALSE); +} + + +static void +on_emergency_call_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + + close_menu (self); + g_action_group_activate_action (G_ACTION_GROUP (phosh_shell_get_default ()), + "emergency.toggle-menu", NULL); +} + + +static void +on_power_menu_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (data); + gboolean locked, show_suspend; + GAction *suspend_action; + + if (self->dialog) { + on_power_menu_done (self); + return; + } + + self->dialog = phosh_power_menu_new (NULL); + gtk_widget_insert_action_group (GTK_WIDGET (self->dialog), "power-menu", + G_ACTION_GROUP (self->menu_actions)); + g_signal_connect_swapped (self->dialog, "done", + G_CALLBACK (on_power_menu_done), self); + + /* Show suspend on lock screen when available */ + locked = phosh_shell_get_locked (phosh_shell_get_default ()); + suspend_action = g_action_map_lookup_action (G_ACTION_MAP (self->menu_actions), "suspend"); + show_suspend = locked && g_action_get_enabled (suspend_action); + phosh_power_menu_set_show_suspend (self->dialog, show_suspend); + + gtk_widget_set_visible (GTK_WIDGET (self->dialog), TRUE); +} + + +static void +on_shell_locked_changed (PhoshPowerMenuManager *self, GParamSpec *pspec, PhoshShell *shell) +{ + GAction *action; + gboolean locked; + + g_return_if_fail (PHOSH_IS_POWER_MENU_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SHELL (shell)); + + locked = phosh_shell_get_locked (shell); + + /* TODO: Make them work on the lock screen too */ + action = g_action_map_lookup_action (G_ACTION_MAP (self->menu_actions), "screen-lock"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), !locked); + action = g_action_map_lookup_action (G_ACTION_MAP (self->menu_actions), "poweroff"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), !locked); +} + + +static void +phosh_power_menu_manager_finalize (GObject *object) +{ + PhoshPowerMenuManager *self = PHOSH_POWER_MENU_MANAGER (object); + + g_action_map_remove_action (G_ACTION_MAP (phosh_shell_get_default ()), "power.toggle-menu"); + + g_clear_object (&self->menu_actions); + g_clear_pointer (&self->dialog, phosh_cp_widget_destroy); + + G_OBJECT_CLASS (phosh_power_menu_manager_parent_class)->finalize (object); +} + + +static void +phosh_power_menu_manager_class_init (PhoshPowerMenuManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_power_menu_manager_finalize; +} + + +static GActionEntry menu_entries[] = { + { .name = "emergency-call", .activate = on_emergency_call_activated }, + { .name = "poweroff", .activate = on_power_off_activated }, + { .name = "screen-lock", .activate = on_screen_lock_activated }, + { .name = "screenshot", .activate = on_screenshot_activated }, + { .name = "suspend", .activate = on_suspend_activated }, +}; + + +static GActionEntry entries[] = { + { .name = "power.toggle-menu", .activate = on_power_menu_activated }, +}; + + +static void +phosh_power_menu_manager_init (PhoshPowerMenuManager *self) +{ + GAction *src_action, *dst_action; + g_autoptr (GSettings) phosh_settings = g_settings_new ("sm.puri.phosh"); + + g_action_map_add_action_entries (G_ACTION_MAP (phosh_shell_get_default ()), + entries, + G_N_ELEMENTS (entries), + self); + + self->menu_actions = g_simple_action_group_new (); + g_action_map_add_action_entries (G_ACTION_MAP (self->menu_actions), + menu_entries, G_N_ELEMENTS (menu_entries), + self); + + /* Only enable emergency call button when the emergency-calls manager says it's o.k. */ + src_action = g_action_map_lookup_action (G_ACTION_MAP (phosh_shell_get_default ()), + "emergency.toggle-menu"); + dst_action = g_action_map_lookup_action (G_ACTION_MAP (self->menu_actions), "emergency-call"); + g_object_bind_property (src_action, "enabled", dst_action, "enabled", G_BINDING_SYNC_CREATE); + + g_settings_bind (phosh_settings, + "enable-suspend", + g_action_map_lookup_action (G_ACTION_MAP (self->menu_actions), "suspend"), + "enabled", + G_SETTINGS_BIND_GET); + + g_signal_connect_object (phosh_shell_get_default (), + "notify::locked", + G_CALLBACK (on_shell_locked_changed), + self, + G_CONNECT_SWAPPED); + on_shell_locked_changed (self, NULL, phosh_shell_get_default ()); +} + + +PhoshPowerMenuManager * +phosh_power_menu_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_POWER_MENU_MANAGER, NULL); +} diff --git a/src/power-menu-manager.h b/src/power-menu-manager.h new file mode 100644 index 000000000..8d67fdbf0 --- /dev/null +++ b/src/power-menu-manager.h @@ -0,0 +1,14 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#define PHOSH_TYPE_POWER_MENU_MANAGER (phosh_power_menu_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshPowerMenuManager, phosh_power_menu_manager, PHOSH, POWER_MENU_MANAGER, GObject); + +PhoshPowerMenuManager *phosh_power_menu_manager_new (void); diff --git a/src/power-menu.c b/src/power-menu.c new file mode 100644 index 000000000..9d590ce25 --- /dev/null +++ b/src/power-menu.c @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-power-menu" + +#include "phosh-config.h" + +#include "fading-label.h" +#include "power-menu.h" +#include "shell-priv.h" +#include "session-manager.h" + +/** + * PhoshPowerMenu: + * + * Menu on power button press + */ + +enum { + PROP_0, + PROP_SHOW_SUSPEND, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = {0}; + + +struct _PhoshPowerMenu { + PhoshSystemModalDialog parent; + + GtkStack *stack; + gboolean show_suspend; +}; +G_DEFINE_TYPE (PhoshPowerMenu, phosh_power_menu, PHOSH_TYPE_SYSTEM_MODAL_DIALOG) + + +static void +on_power_menu_done (PhoshPowerMenu *self) +{ + g_return_if_fail (PHOSH_IS_POWER_MENU (self)); + g_signal_emit (self, signals[DONE], 0); +} + + + +static void +phosh_power_menu_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshPowerMenu *self = PHOSH_POWER_MENU (object); + + switch (property_id) { + case PROP_SHOW_SUSPEND: + phosh_power_menu_set_show_suspend (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_power_menu_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshPowerMenu *self = PHOSH_POWER_MENU (object); + + switch (property_id) { + case PROP_SHOW_SUSPEND: + g_value_set_boolean (value, phosh_power_menu_get_show_suspend (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_power_menu_class_init (PhoshPowerMenuClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_power_menu_get_property; + object_class->set_property = phosh_power_menu_set_property; + + /** + * PowerMenu:show-suspend: + * + * Whether the menu should show the option to suspend the device. Otherwise + * the "power off" option is shown. + */ + props[PROP_SHOW_SUSPEND] = + g_param_spec_boolean ("show-suspend", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + + g_type_ensure (PHOSH_TYPE_FADING_LABEL); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/power-menu.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshPowerMenu, stack); + gtk_widget_class_bind_template_callback (widget_class, on_power_menu_done); + + gtk_widget_class_set_css_name (widget_class, "phosh-power-menu"); +} + + +static void +phosh_power_menu_init (PhoshPowerMenu *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshPowerMenu * +phosh_power_menu_new (PhoshMonitor *monitor) +{ + return PHOSH_POWER_MENU (g_object_new (PHOSH_TYPE_POWER_MENU, + "monitor", monitor, + NULL)); +} + + +void +phosh_power_menu_set_show_suspend (PhoshPowerMenu *self, gboolean show_suspend) +{ + const char *page; + + g_assert (PHOSH_IS_POWER_MENU (self)); + + if (self->show_suspend == show_suspend) + return; + + self->show_suspend = show_suspend; + page = show_suspend ? "suspend" : "poweroff"; + gtk_stack_set_visible_child_name (self->stack, page); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_SUSPEND]); +} + + +gboolean +phosh_power_menu_get_show_suspend (PhoshPowerMenu *self) +{ + g_assert (PHOSH_IS_POWER_MENU (self)); + + return self->show_suspend; +} diff --git a/src/power-menu.h b/src/power-menu.h new file mode 100644 index 000000000..6cacfbcdc --- /dev/null +++ b/src/power-menu.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "system-modal-dialog.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_POWER_MENU (phosh_power_menu_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshPowerMenu, phosh_power_menu, PHOSH, POWER_MENU, PhoshSystemModalDialog) + +PhoshPowerMenu *phosh_power_menu_new (PhoshMonitor *monitor); +void phosh_power_menu_set_show_suspend (PhoshPowerMenu *self, gboolean show_suspend); +gboolean phosh_power_menu_get_show_suspend (PhoshPowerMenu *self); + +G_END_DECLS diff --git a/src/proximity.c b/src/proximity.c new file mode 100644 index 000000000..cc8bb4860 --- /dev/null +++ b/src/proximity.c @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-proximity" + +#include "phosh-config.h" +#include "fader.h" +#include "proximity.h" +#include "shell-priv.h" +#include "sensor-proxy-manager.h" +#include "util.h" + +/** + * PhoshProximity: + * + * Proximity sensor handling + * + * #PhoshProximity handles enabling and disabling the proximity detection + * based on e.g. active calls. + */ + + +enum { + PROP_0, + PROP_SENSOR_PROXY_MANAGER, + PROP_CALLS_MANAGER, + PROP_FADER, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + + +typedef struct _PhoshProximity { + GObject parent; + + gboolean claimed; + PhoshSensorProxyManager *sensor_proxy_manager; + PhoshCallsManager *calls_manager; + PhoshFader *fader; + + GCancellable *cancel; +} PhoshProximity; + +G_DEFINE_TYPE (PhoshProximity, phosh_proximity, G_TYPE_OBJECT); + + +static void +show_fader (PhoshProximity *self, PhoshMonitor *monitor) +{ + if (self->fader) + return; + + self->fader = g_object_new (PHOSH_TYPE_FADER, + "monitor", monitor, + "style-class", "phosh-fader-proximity-fade", + NULL); + gtk_widget_set_visible (GTK_WIDGET (self->fader), TRUE); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FADER]); +} + + +static void +hide_fader (PhoshProximity *self) +{ + if (self->fader == NULL) + return; + + g_clear_pointer (&self->fader, phosh_cp_widget_destroy); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FADER]); +} + + +static void +on_proximity_claimed (PhoshSensorProxyManager *sensor_proxy_manager, + GAsyncResult *res, + PhoshProximity *self) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_claim_proximity_finish ( + PHOSH_DBUS_SENSOR_PROXY (sensor_proxy_manager), + res, &err); + + if (success == FALSE) { + phosh_async_error_warn (err, "Failed to claim proximity sensor"); + return; + } + + g_return_if_fail (PHOSH_IS_PROXIMITY (self)); + g_return_if_fail (sensor_proxy_manager == self->sensor_proxy_manager); + + g_debug ("Claimed proximity sensor"); + self->claimed = TRUE; +} + + +static void +on_proximity_released (PhoshSensorProxyManager *sensor_proxy_manager, + GAsyncResult *res, + PhoshProximity *self) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_release_proximity_finish ( + PHOSH_DBUS_SENSOR_PROXY(sensor_proxy_manager), + res, &err); + + if (success == FALSE) { + if (!phosh_async_error_warn (err, "Failed to release proximity sensor")) { + /* If not canceled hide fader */ + hide_fader (self); + } + return; + } + + g_debug ("Released proximity sensor"); + self->claimed = FALSE; + + hide_fader (self); +} + + +static void +phosh_proximity_claim_proximity (PhoshProximity *self, gboolean claim) +{ + if (claim == self->claimed) + return; + + if (claim) { + phosh_dbus_sensor_proxy_call_claim_proximity ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), + self->cancel, + (GAsyncReadyCallback)on_proximity_claimed, + self); + } else { + phosh_dbus_sensor_proxy_call_release_proximity ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), + self->cancel, + (GAsyncReadyCallback)on_proximity_released, + self); + } +} + + +static void +on_has_proximity_changed (PhoshProximity *self, + GParamSpec *pspec, + PhoshSensorProxyManager *proxy) +{ + gboolean has_proximity; + + has_proximity = phosh_dbus_sensor_proxy_get_has_proximity ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + g_debug ("Found %s proximity sensor", has_proximity ? "a" : "no"); + + /* If the proxy went a way we always unclaim but only claim on ongoing calls: */ + if (!phosh_calls_manager_get_active_call_handle (self->calls_manager) && has_proximity) + return; + + phosh_proximity_claim_proximity (self, has_proximity); +} + + +static void +on_calls_manager_active_call_changed (PhoshProximity *self, + GParamSpec *pspec, + PhoshCallsManager *calls_manager) +{ + gboolean active; + + g_return_if_fail (PHOSH_IS_PROXIMITY (self)); + g_return_if_fail (PHOSH_IS_CALLS_MANAGER (calls_manager)); + + active = !!phosh_calls_manager_get_active_call_handle (self->calls_manager); + phosh_proximity_claim_proximity (self, active); + /* TODO: if call is over wait until we hit the threshold */ +} + + +static void +on_proximity_near_changed (PhoshProximity *self, + GParamSpec *pspec, + PhoshSensorProxyManager *sensor) +{ + gboolean near; + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitor *monitor = phosh_shell_get_builtin_monitor (shell); + + if (!self->claimed) + return; + + near = phosh_dbus_sensor_proxy_get_proximity_near ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + g_debug ("Proximity near changed: %d", near); + if (near && monitor) + show_fader (self, monitor); + else + hide_fader (self); +} + +static void +phosh_proximity_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshProximity *self = PHOSH_PROXIMITY (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + /* construct only */ + self->sensor_proxy_manager = g_value_dup_object (value); + break; + case PROP_CALLS_MANAGER: + /* construct only */ + self->calls_manager = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_proximity_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshProximity *self = PHOSH_PROXIMITY (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + g_value_set_object (value, self->sensor_proxy_manager); + break; + case PROP_CALLS_MANAGER: + g_value_set_object (value, self->calls_manager); + break; + case PROP_FADER: + g_value_set_boolean (value, !!self->fader); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_proximity_constructed (GObject *object) +{ + PhoshProximity *self = PHOSH_PROXIMITY (object); + + g_signal_connect_swapped (self->calls_manager, + "notify::active-call", + G_CALLBACK (on_calls_manager_active_call_changed), + self); + + g_signal_connect_swapped (self->sensor_proxy_manager, + "notify::proximity-near", + (GCallback) on_proximity_near_changed, + self); + + g_signal_connect_swapped (self->sensor_proxy_manager, + "notify::has-proximity", + (GCallback) on_has_proximity_changed, + self); + on_has_proximity_changed (self, NULL, self->sensor_proxy_manager); + + G_OBJECT_CLASS (phosh_proximity_parent_class)->constructed (object); +} + + +static void +phosh_proximity_dispose (GObject *object) +{ + PhoshProximity *self = PHOSH_PROXIMITY (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (self->sensor_proxy_manager) { + g_signal_handlers_disconnect_by_data (self->sensor_proxy_manager, + self); + phosh_dbus_sensor_proxy_call_release_proximity_sync ( + PHOSH_DBUS_SENSOR_PROXY(self->sensor_proxy_manager), NULL, NULL); + g_clear_object (&self->sensor_proxy_manager); + } + + if (self->calls_manager) { + g_signal_handlers_disconnect_by_data (self->calls_manager, + self); + g_clear_object (&self->calls_manager); + } + + g_clear_pointer (&self->fader, phosh_cp_widget_destroy); + G_OBJECT_CLASS (phosh_proximity_parent_class)->dispose (object); +} + + +static void +phosh_proximity_class_init (PhoshProximityClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + + object_class->constructed = phosh_proximity_constructed; + object_class->dispose = phosh_proximity_dispose; + + object_class->set_property = phosh_proximity_set_property; + object_class->get_property = phosh_proximity_get_property; + + props[PROP_SENSOR_PROXY_MANAGER] = + g_param_spec_object ( + "sensor-proxy-manager", + "Sensor proxy manager", + "The object inerfacing with iio-sensor-proxy", + PHOSH_TYPE_SENSOR_PROXY_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_CALLS_MANAGER] = + g_param_spec_object ( + "calls-manager", + "", + "", + PHOSH_TYPE_CALLS_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /* PhoshProximity:fader: + * + * %TRUE if the fader to prevent accidental user input is currently active + */ + props[PROP_FADER] = + g_param_spec_boolean ( + "fader", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); + +} + + +static void +phosh_proximity_init (PhoshProximity *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshProximity * +phosh_proximity_new (PhoshSensorProxyManager *sensor_proxy_manager, + PhoshCallsManager *calls_manager) +{ + return g_object_new (PHOSH_TYPE_PROXIMITY, + "sensor-proxy-manager", sensor_proxy_manager, + "calls-manager", calls_manager, + NULL); +} + +gboolean +phosh_proximity_has_fader (PhoshProximity *self) +{ + g_return_val_if_fail (PHOSH_IS_PROXIMITY (self), FALSE); + + return !!self->fader; +} diff --git a/src/proximity.h b/src/proximity.h new file mode 100644 index 000000000..b742d3824 --- /dev/null +++ b/src/proximity.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "calls-manager.h" +#include "sensor-proxy-manager.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_PROXIMITY (phosh_proximity_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshProximity, phosh_proximity, PHOSH, PROXIMITY, GObject); + +PhoshProximity *phosh_proximity_new (PhoshSensorProxyManager *sensor_proxy_manager, + PhoshCallsManager *calls_manager); +gboolean phosh_proximity_has_fader (PhoshProximity *sensor_proxy_manager); + +G_END_DECLS diff --git a/src/quick-setting.c b/src/quick-setting.c new file mode 100644 index 000000000..e68cf87b3 --- /dev/null +++ b/src/quick-setting.c @@ -0,0 +1,709 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Julian Sparber + * Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-quick-setting" + +#include "phosh-config.h" + +#include "quick-setting.h" + +/** + * PhoshQuickSetting: + * + * A `PhoshQuickSetting` represents a state of an entity (like Wi-Fi, Bluetooth) using an icon + * and label. It should be added to a PhoshQuickSettingsBox for better integration. + * + * A quick-setting displays the state using an icon and label. The state is set by + * [class@Phosh.StatusIcon], which must be set as a [property@Phosh.QuickSetting:status-icon]. It + * can also have a status-page, which can be used to expose additional features. For example, a + * Wi-Fi quick-setting can show available Wi-Fi hotspots as an extra option. When a status widget is + * set, the quick-setting displays an arrow at the right end. + * + * A quick-setting itself does not have any provision to display its status-page. It is + * completely upto the user to display and hide the status-pages as required. However the + * quick-setting can aid in the task with its [property@Phosh.QuickSetting:showing-status] property. + * When `showing-status` is false, clicking the arrow will cause the quick-setting to emit + * [signal@Phosh.QuickSetting::show-status]. If `showing-status` is true, then it will emit + * [signal@Phosh.QuickSetting::hide-status]. The user of the quick-setting is expected to follow + * this convention and set `showing-status` based on whether they are displaying the status-page + * or not. + * + * A quick-setting might be temporarily prevented from showing its status-page using + * [property@Phosh.QuickSetting:can-show-status]. Again, `PhoshQuickSettingsBox` can take care of + * this property, such that once you tell the box if showing status-page is allowed, it will ensure + * that the children's `can-show-status` are synchronized with it. + * + * A quick-setting can be in an active or inactive state. However clicking the quick-setting does + * not toggle its state. The user must set the state using [property@Phosh.QuickSetting:active]. If + * the status-icon [class@StatusIcon] has an `enabled` property it will be automatically bound to + * the [property@Phosh.QuickSetting:active] property. + * + * When a quick-setting is clicked, [signal@Phosh.QuickSetting::clicked] is emitted. When it is + * long-pressed or right-clicked, [signal@PhoshQuickSetting::long-pressed] is emitted. + * + * The common usecase of `long-pressed` is to launch an action (like `panel.launch-panel`). So to + * avoid duplicating this process for each quick-setting, the user can set + * [property@Phosh.QuickSetting:long-press-action-name] and + * [property@Phosh.QuickSetting:long-press-action-target]. The quick-setting then launches that + * appropriate action. + */ + +enum { + PROP_0, + PROP_ACTIVE, + PROP_SHOWING_STATUS, + PROP_CAN_SHOW_STATUS, + PROP_STATUS_ICON, + PROP_STATUS_PAGE, + PROP_LONG_PRESS_ACTION_NAME, + PROP_LONG_PRESS_ACTION_TARGET, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + CLICKED, + LONG_PRESSED, + SHOW_STATUS, + HIDE_STATUS, + N_SIGNALS, +}; +static guint signals[N_SIGNALS]; + +typedef struct { + GtkBox *box; + GtkLabel *label; + GtkButton *arrow_btn; + GtkImage *arrow; + GtkGesture *long_press; + GtkGesture *multi_press; + + gboolean active; + gboolean showing_status; + gboolean can_show_status; + PhoshStatusPage *status_page; + PhoshStatusIcon *status_icon; + char *long_press_action_name; + char *long_press_action_target; + + GBinding *active_binding; + GBinding *label_binding; +} PhoshQuickSettingPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshQuickSetting, phosh_quick_setting, GTK_TYPE_BOX); + + +static void +phosh_quick_setting_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshQuickSetting *self = PHOSH_QUICK_SETTING (object); + + switch (property_id) { + case PROP_ACTIVE: + phosh_quick_setting_set_active (self, g_value_get_boolean (value)); + break; + case PROP_SHOWING_STATUS: + phosh_quick_setting_set_showing_status (self, g_value_get_boolean (value)); + break; + case PROP_CAN_SHOW_STATUS: + phosh_quick_setting_set_can_show_status (self, g_value_get_boolean (value)); + break; + case PROP_STATUS_ICON: + phosh_quick_setting_set_status_icon (self, g_value_get_object (value)); + break; + case PROP_STATUS_PAGE: + phosh_quick_setting_set_status_page (self, g_value_get_object (value)); + break; + case PROP_LONG_PRESS_ACTION_NAME: + phosh_quick_setting_set_long_press_action_name (self, g_value_get_string (value)); + break; + case PROP_LONG_PRESS_ACTION_TARGET: + phosh_quick_setting_set_long_press_action_target (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_quick_setting_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshQuickSetting *self = PHOSH_QUICK_SETTING (object); + + switch (property_id) { + case PROP_ACTIVE: + g_value_set_boolean (value, phosh_quick_setting_get_active (self)); + break; + case PROP_SHOWING_STATUS: + g_value_set_boolean (value, phosh_quick_setting_get_showing_status (self)); + break; + case PROP_CAN_SHOW_STATUS: + g_value_set_boolean (value, phosh_quick_setting_get_can_show_status (self)); + break; + case PROP_STATUS_ICON: + g_value_set_object (value, phosh_quick_setting_get_status_icon (self)); + break; + case PROP_STATUS_PAGE: + g_value_set_object (value, phosh_quick_setting_get_status_page (self)); + break; + case PROP_LONG_PRESS_ACTION_NAME: + g_value_set_string (value, phosh_quick_setting_get_long_press_action_name (self)); + break; + case PROP_LONG_PRESS_ACTION_TARGET: + g_value_set_string (value, phosh_quick_setting_get_long_press_action_target (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +on_status_icon_destroy (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv = phosh_quick_setting_get_instance_private (self); + priv->status_icon = NULL; +} + + +static void +launch_action_else_emit (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv = phosh_quick_setting_get_instance_private (self); + GActionGroup *group; + GVariant *param = NULL; + g_auto (GStrv) str_array = NULL; + + if (priv->long_press_action_name == NULL) { + g_signal_emit (self, signals[LONG_PRESSED], 0); + return; + } + + if (priv->long_press_action_target != NULL) + param = g_variant_new_parsed (priv->long_press_action_target, NULL); + + str_array = g_strsplit (priv->long_press_action_name, ".", 2); + if (g_strv_length (str_array) != 2) { + g_warning ("Malformed action-name %s", priv->long_press_action_name); + return; + } + + group = gtk_widget_get_action_group (GTK_WIDGET (self), str_array[0]); + g_return_if_fail (group); + g_action_group_activate_action (group, str_array[1], param); +} + + +static void +on_long_pressed (PhoshQuickSetting *self, GtkGesture *gesture) +{ + launch_action_else_emit (self); + gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED); +} + + +static void +on_right_pressed (PhoshQuickSetting *self, int n_press, double x, double y, GtkGesture *gesture) +{ + launch_action_else_emit (self); + gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED); +} + + +static void +on_button_clicked (PhoshQuickSetting *self) +{ + g_signal_emit (self, signals[CLICKED], 0); +} + + +static void +on_arrow_clicked (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv = phosh_quick_setting_get_instance_private (self); + + if (priv->showing_status) + g_signal_emit (self, signals[HIDE_STATUS], 0); + else + g_signal_emit (self, signals[SHOW_STATUS], 0); +} + + +static void +on_status_destroy (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv = phosh_quick_setting_get_instance_private (self); + g_clear_object (&priv->status_page); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATUS_PAGE]); +} + + +static void +on_status_page_done (PhoshQuickSetting *self) +{ + g_signal_emit (self, signals[HIDE_STATUS], 0); +} + + +static void +phosh_quick_setting_finalize (GObject *object) +{ + PhoshQuickSetting *self = PHOSH_QUICK_SETTING (object); + PhoshQuickSettingPrivate *priv = phosh_quick_setting_get_instance_private (self); + + g_clear_object (&priv->status_page); + g_clear_pointer (&priv->long_press_action_name, g_free); + g_clear_pointer (&priv->long_press_action_target, g_free); + + G_OBJECT_CLASS (phosh_quick_setting_parent_class)->finalize (object); +} + + +static void +phosh_quick_setting_destroy (GtkWidget *widget) +{ + PhoshQuickSetting *self = PHOSH_QUICK_SETTING (widget); + + phosh_quick_setting_set_status_icon (self, NULL); + + GTK_WIDGET_CLASS (phosh_quick_setting_parent_class)->destroy (widget); +} + + +static void +phosh_quick_setting_class_init (PhoshQuickSettingClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_quick_setting_set_property; + object_class->get_property = phosh_quick_setting_get_property; + object_class->finalize = phosh_quick_setting_finalize; + + widget_class->destroy = phosh_quick_setting_destroy; + + /** + * PhoshQuickSetting:active: + * + * The active state of the child. + */ + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:showing-status: + * + * If the child is displaying its status. + */ + props[PROP_SHOWING_STATUS] = + g_param_spec_boolean ("showing-status", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:can-show-status: + * + * If the child can display its status. + */ + props[PROP_CAN_SHOW_STATUS] = + g_param_spec_boolean ("can-show-status", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:status-icon: + * + * The status-icon. + */ + props[PROP_STATUS_ICON] = + g_param_spec_object ("status-icon", "", "", + PHOSH_TYPE_STATUS_ICON, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:status-page: + * + * The status-page. + */ + props[PROP_STATUS_PAGE] = + g_param_spec_object ("status-page", "", "", + PHOSH_TYPE_STATUS_PAGE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:long-press-action-name: + * + * Action name to trigger on long-press. + */ + props[PROP_LONG_PRESS_ACTION_NAME] = + g_param_spec_string ("long-press-action-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSetting:long-press-action-target: + * + * Action target for `long-press-action-name`. + */ + props[PROP_LONG_PRESS_ACTION_TARGET] = + g_param_spec_string ("long-press-action-target", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[CLICKED] = g_signal_new ("clicked", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + signals[LONG_PRESSED] = g_signal_new ("long-pressed", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + signals[SHOW_STATUS] = g_signal_new ("show-status", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + signals[HIDE_STATUS] = g_signal_new ("hide-status", + G_OBJECT_CLASS_TYPE (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/quick-setting.ui"); + + gtk_widget_class_bind_template_callback (widget_class, on_arrow_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_button_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_long_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_right_pressed); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, box); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, label); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, arrow); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, arrow_btn); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, long_press); + gtk_widget_class_bind_template_child_private (widget_class, PhoshQuickSetting, multi_press); + + gtk_widget_class_set_css_name (widget_class, "phosh-quick-setting"); +} + + +static void +phosh_quick_setting_init (PhoshQuickSetting *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + +GtkWidget * +phosh_quick_setting_new (PhoshStatusPage *status_page) +{ + return GTK_WIDGET (g_object_new (PHOSH_TYPE_QUICK_SETTING, "status-page", status_page, NULL)); +} + + +void +phosh_quick_setting_set_active (PhoshQuickSetting *self, gboolean active) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + + priv = phosh_quick_setting_get_instance_private (self); + + if (priv->active == active) + return; + + priv->active = active; + + if (priv->active) + gtk_widget_set_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED, FALSE); + else + gtk_widget_unset_state_flags (GTK_WIDGET (self), GTK_STATE_FLAG_CHECKED); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTIVE]); +} + + +gboolean +phosh_quick_setting_get_active (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), FALSE); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->active; +} + + +void +phosh_quick_setting_set_showing_status (PhoshQuickSetting *self, gboolean showing_status) +{ + PhoshQuickSettingPrivate *priv; + const char *icon_name; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + + priv = phosh_quick_setting_get_instance_private (self); + + if (priv->showing_status == showing_status) + return; + + priv->showing_status = showing_status; + + if (priv->showing_status) + icon_name = "go-down-symbolic"; + else + icon_name = "go-next-symbolic"; + + gtk_image_set_from_icon_name (priv->arrow, icon_name, -1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOWING_STATUS]); +} + + +gboolean +phosh_quick_setting_get_showing_status (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), FALSE); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->showing_status; +} + + +void +phosh_quick_setting_set_can_show_status (PhoshQuickSetting *self, gboolean can_show_status) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + + priv = phosh_quick_setting_get_instance_private (self); + + if (priv->can_show_status == can_show_status) + return; + + priv->can_show_status = can_show_status; + gtk_widget_set_visible (GTK_WIDGET (priv->arrow_btn), + priv->status_page != NULL && priv->can_show_status); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SHOW_STATUS]); +} + + +gboolean +phosh_quick_setting_get_can_show_status (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), FALSE); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->can_show_status; +} + +/** + * phosh_quick_setting_set_status_icon: + * @self: A quick-setting + * @status_icon: A status-icon or `NULL` + * + * Set the status-icon of the quick-setting. Use `NULL` to remove existing icon. + */ +void +phosh_quick_setting_set_status_icon (PhoshQuickSetting *self, PhoshStatusIcon *status_icon) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + g_return_if_fail (status_icon == NULL || PHOSH_IS_STATUS_ICON (status_icon)); + + priv = phosh_quick_setting_get_instance_private (self); + + if (priv->status_icon == status_icon) + return; + + if (priv->status_icon) { + g_clear_pointer (&priv->label_binding, g_binding_unbind); + g_clear_pointer (&priv->active_binding, g_binding_unbind); + gtk_container_remove (GTK_CONTAINER (priv->box), GTK_WIDGET (priv->status_icon)); + } + + priv->status_icon = status_icon; + + if (priv->status_icon) { + priv->label_binding = g_object_bind_property (status_icon, "info", + priv->label, "label", + G_BINDING_SYNC_CREATE); + + if (g_object_class_find_property (G_OBJECT_GET_CLASS (status_icon), "enabled")) { + priv->active_binding = g_object_bind_property (status_icon, "enabled", + self, "active", + G_BINDING_SYNC_CREATE); + } + + g_signal_connect_object (status_icon, "destroy", G_CALLBACK (on_status_icon_destroy), self, + G_CONNECT_SWAPPED); + + gtk_box_pack_start (priv->box, GTK_WIDGET (priv->status_icon), 0, 0, 0); + gtk_box_reorder_child (priv->box, GTK_WIDGET (priv->status_icon), 0); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATUS_PAGE]); +} + +/** + * phosh_quick_setting_get_status_icon: + * @self: A quick-setting + * + * Get the current status-icon of the quick-setting. + * + * Returns:(transfer none): The status-icon or `NULL`. + */ +PhoshStatusIcon * +phosh_quick_setting_get_status_icon (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), NULL); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->status_icon; +} + + +/** + * phosh_quick_setting_set_status_page: + * @self: A quick-setting + * @status_page: A status-page or `NULL` + * + * Set the status-page of the quick-setting. + */ +void +phosh_quick_setting_set_status_page (PhoshQuickSetting *self, PhoshStatusPage *status_page) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + g_return_if_fail (status_page == NULL || PHOSH_IS_STATUS_PAGE (status_page)); + + priv = phosh_quick_setting_get_instance_private (self); + + if (priv->status_page == status_page) + return; + + if (priv->status_page != NULL) { + g_signal_handlers_disconnect_by_data (priv->status_page, self); + g_clear_object (&priv->status_page); + } + + priv->status_page = status_page; + if (priv->status_page != NULL) { + g_object_ref_sink (priv->status_page); + g_signal_connect_object (priv->status_page, + "destroy", + G_CALLBACK (on_status_destroy), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (priv->status_page, + "done", + G_CALLBACK (on_status_page_done), + self, + G_CONNECT_SWAPPED); + } + + gtk_widget_set_visible (GTK_WIDGET (priv->arrow_btn), + priv->status_page != NULL && priv->can_show_status); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATUS_PAGE]); +} + + +/** + * phosh_quick_setting_get_status_page: + * @self: A quick-setting + * + * Get the current status widget of the quick-setting. + * + * Returns:(transfer none): The status-page or `NULL`. + */ +PhoshStatusPage * +phosh_quick_setting_get_status_page (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), NULL); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->status_page; +} + + +void +phosh_quick_setting_set_long_press_action_name (PhoshQuickSetting *self, const char *action_name) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + + priv = phosh_quick_setting_get_instance_private (self); + + g_clear_pointer (&priv->long_press_action_name, g_free); + priv->long_press_action_name = g_strdup (action_name); +} + + +const char * +phosh_quick_setting_get_long_press_action_name (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), NULL); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->long_press_action_name; +} + + +void +phosh_quick_setting_set_long_press_action_target (PhoshQuickSetting *self, const char *action_target) +{ + PhoshQuickSettingPrivate *priv; + + g_return_if_fail (PHOSH_IS_QUICK_SETTING (self)); + + priv = phosh_quick_setting_get_instance_private (self); + + g_clear_pointer (&priv->long_press_action_target, g_free); + priv->long_press_action_target = g_strdup (action_target); +} + + +const char * +phosh_quick_setting_get_long_press_action_target (PhoshQuickSetting *self) +{ + PhoshQuickSettingPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_QUICK_SETTING (self), NULL); + + priv = phosh_quick_setting_get_instance_private (self); + + return priv->long_press_action_target; +} diff --git a/src/quick-setting.h b/src/quick-setting.h new file mode 100644 index 000000000..ac02472e0 --- /dev/null +++ b/src/quick-setting.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "status-icon.h" +#include "status-page.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_QUICK_SETTING phosh_quick_setting_get_type () +G_DECLARE_DERIVABLE_TYPE (PhoshQuickSetting, phosh_quick_setting, PHOSH, QUICK_SETTING, GtkBox) + +struct _PhoshQuickSettingClass { + GtkBoxClass parent_class; + + /* Padding for future expansion */ + void (*_phosh_reserved0) (void); + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + +GtkWidget *phosh_quick_setting_new (PhoshStatusPage *status_page); +void phosh_quick_setting_set_active (PhoshQuickSetting *self, gboolean active); +gboolean phosh_quick_setting_get_active (PhoshQuickSetting *self); +void phosh_quick_setting_set_can_show_status (PhoshQuickSetting *self, gboolean can_show_status); +gboolean phosh_quick_setting_get_can_show_status (PhoshQuickSetting *self); +void phosh_quick_setting_set_showing_status (PhoshQuickSetting *self, gboolean showing_status); +gboolean phosh_quick_setting_get_showing_status (PhoshQuickSetting *self); +void phosh_quick_setting_set_status_icon (PhoshQuickSetting *self, PhoshStatusIcon *status_icon); +PhoshStatusIcon *phosh_quick_setting_get_status_icon (PhoshQuickSetting *self); +void phosh_quick_setting_set_status_page (PhoshQuickSetting *self, PhoshStatusPage *status_page); +PhoshStatusPage *phosh_quick_setting_get_status_page (PhoshQuickSetting *self); +void phosh_quick_setting_set_long_press_action_name (PhoshQuickSetting *self, const char *action_name); +const char *phosh_quick_setting_get_long_press_action_name (PhoshQuickSetting *self); +void phosh_quick_setting_set_long_press_action_target (PhoshQuickSetting *self, const char *action_target); +const char *phosh_quick_setting_get_long_press_action_target (PhoshQuickSetting *self); + +G_END_DECLS diff --git a/src/quick-settings-box.c b/src/quick-settings-box.c new file mode 100644 index 000000000..14cc011d8 --- /dev/null +++ b/src/quick-settings-box.c @@ -0,0 +1,911 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-quick-settings-box" + +#include "phosh-config.h" + +#include "quick-settings-box.h" + +#include + +#define MAX_CHILD_WIDTH 150 + +/** + * PhoshQuickSettingsBox: + * + * A `PhoshQuickSettingsBox` displays [class@Phosh.QuickSetting] in a responsive grid. + * + * It takes care of displaying the quick-settings' status-pages as per + * [signal@Phosh.QuickSetting::show-status] and [signal@Phosh.QuickSetting::hide-status]. At a time, + * only zero or one status-page is displayed. + * + * Use [property@Phosh.QuickSettingsBox:can-show-status] to temporarily prevent displaying of status + * widgets, like in a lock-screen. + * + * Due to the inclusion of status-pages, the box's height grows dynamically. For best usage, it is + * advised to add the box to a vertically-scrollable parent. + */ + +enum { + PROP_0, + PROP_MAX_COLUMNS, + PROP_SPACING, + PROP_CAN_SHOW_STATUS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshQuickSettingsBox { + GtkContainer parent; + + guint max_columns; + guint spacing; + gboolean can_show_status; + + GPtrArray *children; + GtkRevealer *revealer; + PhoshQuickSetting *shown_child; + PhoshQuickSetting *to_show_child; +}; + +G_DEFINE_TYPE (PhoshQuickSettingsBox, phosh_quick_settings_box, GTK_TYPE_CONTAINER); + + +static void +phosh_quick_settings_box_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (object); + + switch (property_id) { + case PROP_MAX_COLUMNS: + phosh_quick_settings_box_set_max_columns (self, g_value_get_uint (value)); + break; + case PROP_SPACING: + phosh_quick_settings_box_set_spacing (self, g_value_get_uint (value)); + break; + case PROP_CAN_SHOW_STATUS: + phosh_quick_settings_box_set_can_show_status (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_quick_settings_box_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (object); + + switch (property_id) { + case PROP_MAX_COLUMNS: + g_value_set_uint (value, phosh_quick_settings_box_get_max_columns (self)); + break; + case PROP_SPACING: + g_value_set_uint (value, phosh_quick_settings_box_get_spacing (self)); + break; + case PROP_CAN_SHOW_STATUS: + g_value_set_boolean (value, phosh_quick_settings_box_get_can_show_status (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_quick_settings_box_destroy (GtkWidget *widget) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (widget); + + GTK_WIDGET_CLASS (phosh_quick_settings_box_parent_class)->destroy (widget); + + if (self->children != NULL) { + g_ptr_array_free (self->children, TRUE); + self->children = NULL; + } + + if (self->revealer != NULL) { + gtk_widget_unparent (GTK_WIDGET (self->revealer)); + self->revealer = NULL; + } +} + + +/* + * Allocation Algorithm: + * + * 1. Structure: + * - PhoshQuickSettingsBox has a list of PhoshQuickSetting and a revealer. + * - The revealer is an internal child, which is used to show the status-pages. + * - Given a rectangular region, our aim is to display all the (visible) quick-settings as a grid, + * whose maximum number of columns is the box's columns property and whose rows and columns are + * separated by box's spacing property. + * - Whenever a child wants to show its status-page, we add it to our revealer and reveal the + * revealer below the child's row. + * - Whenever we mention children, we mean only the visible children. + * - By height of the quick-settings, we mean the maximum of height of all quick-settings. + * - Same for width, except that the width is capped by `MAX_CHILD_WIDTH` to prevent lengthy labels + * from forcing single column. + * + * 2. Spacing + * - 1 child has 0 spacing. + * - 2 children have 1 spacing. + * - 3 children have 2 spacing. + * - N children have N - 1 spacing. + * - In general, the value of a measurement (height or width) = + * number of fields (rows or columns) * value of child + (number of fields - 1) * spacing. + * - Or in a concise form = + * number of fields * (value of child + spacing) - spacing. + * - To keep the rest of algorithm description simple, we use "total spacing" to refer to the total + * spacing among the rows or columns. + * + * 3. Request mode + * - PhoshQuickSettingsBox uses height-for-width request mode. + * - Gtk asks for our preferred width first. + * - Then it gives us a width and asks for our preferred height. + * + * 4. Preferred width + * - The preferred width of the box is computed using the natural width of the quick-settings. + * - The minimum width is just the child width, i.e. equivalent of a single column. + * - The natural width is number of columns * child width + total spacing. + * - If the revealer is revealed, then the values are maximum of its minimum and natural width. + * + * 5. Preferred height + * - The preferred height of the box is computed using the natural height and width of the + * quick-settings. + * - We divide the given width with the child's width (with spacing also taken into account). + * - This gives us the number of columns. + * - The columns are then reassigned as the minimum of this value and the box's columns property. + * - The rows are then determined as the number of children / columns. + * - Both the minimum and natural height are set as number of rows * child height + total spacing. + * - If the revealer is revealed, then the values are added with its minimum and natural height. + * + * 6. Actual allocation + * - There are two kinds of allocation possible. + * - Either Gtk gives us equal or more than the height and width we requested. + * - Or it gives less than what we asked. + * - This equality is determined in the same way as preferred height. + * - We use the given allocation's width to compute the number of columns and rows. + * - Then we check if the allocation's height is sufficient for the number of rows we have. + * + * 7. Equal or more + * - In this case, we divide the extra width by the number of columns and distribute it equally to + * each quick-setting. + * - For height, we get the extra height. + * - We let the revealer take the minimum of its height and the extra height. + * - Then the remaining extra height is divided by the number of rows and is then equally + * distributed to each quick-setting. + * + * 8. Less + * - In this case, the number of columns is 1 and the number of rows is the + * number of children. + * - Then each quick-setting is given the full allocation width. + * - Then we subtract the revealer's height from allocation's height. + * - After this, each quick-setting is given a height that is determined by dividing this new height + * by the number of rows (with spacing also taken into account). + * + * 9. Revealing status-pages + * - Revealing status-page may not be immediate. + * - If the box is not showing any status-page, then any request to show status-page, happens + * immediately. + * - But if the box is already showing a status-page, then a request from other child to show its + * status-page gets queued. + * - By queue, we mean that the new child is stored in `to_show_child` variable. + * - We ask the revealer to unreveal itself and wait for it to complete the transition (using + * `child-revealed` signal). + * - Then we ask the existing child to hide its status-page. + * - Once the transition is complete, we add the next child's status-page and reveal it. + * + * 10. Immediate hiding of status-page + * - For some scenarios, status-page are immediately hidden instead of the usual removal after + * transition. + * - This is done by removing the status-page from the revealer, setting `showing-status` on the + * shown child to false and then setting `shown_child` to `NULL`. + * - After this the revealer's `reveal-child` is set to false. + * - This immediate removal happens when the shown child's sensitivity or visibility changes, + * status-page changes to `NULL` or the child itself is removed from the box. + * - When the status-page changes to a valid page, we hot-swap the old page with new one + * without any transition. + * + * 11. Bottom margin on status-pages + * - We set `spacing` as the bottom margin of status-pages. + * - To understand why we don't request extra height and allocate spacing from it, consider the + * below scenario. + * - Let's say we have two quick-settings A and B, arranged in a single column. + * - With A's status-page revealed, the structure will be like: A, spacing, status-page, spacing, B. + * - Now, when the status-page is unrevealed, it happens gradually. + * - At the final step of unrevealing, there could be just 1px height of status-page. + * - But at the same time, the spacing before and after status-page is still there. + * - Now, when the status-page gets completely hidden, the structure will be like: A, spacing, B. + * - That is, a `spacing` amount of pixels suddenly disappears. + * - As the value of `spacing` increases, the visual flicker caused by this disappearance gets more + * noticeable. + * - To avoid this, we need to make the spacing also disappear gradually. + * - An easy way to do it is by making that spacing part of margin of the status-page. + * - This way, the revealer will ensure that entire structure of status-page with bottom margin + * reveals and unreveals smoothly. + */ + +static int +compute_child_height (PhoshQuickSettingsBox *self) +{ + int height = 0; + int nat_height = 0; + + for (int i = 0; i < self->children->len; i++) { + gtk_widget_get_preferred_height (g_ptr_array_index (self->children, i), NULL, &nat_height); + height = MAX (height, nat_height); + } + + return height; +} + + +static int +compute_child_width (PhoshQuickSettingsBox *self) +{ + int width = 0; + int nat_width = 0; + + for (int i = 0; i < self->children->len; i++) { + gtk_widget_get_preferred_width (g_ptr_array_index (self->children, i), NULL, &nat_width); + width = MAX (width, nat_width); + } + + return MIN (width, MAX_CHILD_WIDTH); +} + + +static GtkSizeRequestMode +phosh_quick_settings_box_get_request_mode (GtkWidget *widget) +{ + g_debug ("%p: Querying for request mode", widget); + return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH; +} + + +static void +phosh_quick_settings_box_get_preferred_width (GtkWidget *widget, int *minimum_width, + int *natural_width) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (widget); + int len = 0; + int child_width; + int cols; + + g_debug ("%p: Querying for preferred width", self); + + for (int i = 0; i < self->children->len; i++) + if (gtk_widget_get_visible (g_ptr_array_index (self->children, i))) + len += 1; + + if (len == 0) { + g_debug ("%p: No visible children so preferred width is 0", self); + *natural_width = 0; + *minimum_width = 0; + return; + } + + g_debug ("%p: Configuration: children = %d/%d\tcolumns = %d\tspacing = %d", + self, len, self->children->len, self->max_columns, self->spacing); + + child_width = compute_child_width (self); + cols = self->max_columns; + + g_debug ("%p: Computed child width = %d", self, child_width); + + *minimum_width = child_width; + *natural_width = cols * (child_width + self->spacing) - self->spacing; + + g_debug ("%p: Computed preferred width: minimum = %d\tnatural = %d", + self, *minimum_width, *natural_width); + + if (self->shown_child != NULL) { + int rev_min_width = 0; + int rev_nat_width = 0; + gtk_widget_get_preferred_width (GTK_WIDGET (self->revealer), &rev_min_width, &rev_nat_width); + *minimum_width = MAX (*minimum_width, rev_min_width); + *natural_width = MAX (*natural_width, rev_nat_width); + + g_debug ("%p: Revealer preferred width: minimum = %d\tnatural = %d", + self, rev_min_width, rev_nat_width); + g_debug ("%p: Adjusted preferred width: minimum = %d\tnatural = %d", + self, *minimum_width, *natural_width); + } +} + + +static void +phosh_quick_settings_box_get_preferred_height_for_width (GtkWidget *widget, int width, + int *minimum_height, int *natural_height) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (widget); + int len = 0; + int child_width; + int child_height; + int cols; + int rows; + + g_debug ("%p: Querying preferred height for width = %d", self, width); + + for (int i = 0; i < self->children->len; i++) + if (gtk_widget_get_visible (g_ptr_array_index (self->children, i))) + len += 1; + + if (len == 0) { + g_debug ("%p: No visible children so preferred height is 0", self); + *natural_height = 0; + *minimum_height = 0; + return; + } + + g_debug ("%p: Configuration: children = %d/%d\tcolumns = %d\tspacing = %d", + self, len, self->children->len, self->max_columns, self->spacing); + + child_width = compute_child_width (self); + child_height = compute_child_height (self); + cols = (int) floor ((float) (width + self->spacing) / (child_width + self->spacing)); + cols = MIN (cols, self->max_columns); + rows = (int) ceil ((float) len / cols); + + g_debug ("%p: Computed children values: width = %d\theight = %d\tcols = %d\trows = %d", + self, child_width, child_height, cols, rows); + + g_return_if_fail (cols > 0 && rows > 0); + + *minimum_height = rows * (child_height + self->spacing) - self->spacing; + *natural_height = *minimum_height; + + g_debug ("%p: Computed preferred height: minimum = %d\tnatural = %d", + self, *minimum_height, *natural_height); + + if (self->shown_child != NULL) { + int rev_min_height = 0; + int rev_nat_height = 0; + gtk_widget_get_preferred_height_for_width (GTK_WIDGET (self->revealer), width, &rev_min_height, + &rev_nat_height); + *minimum_height += rev_min_height; + *natural_height += rev_nat_height; + + g_debug ("%p: Revealer preferred height: minimum = %d\tnatural = %d", + self, rev_min_height, rev_nat_height); + g_debug ("%p: Adjusted preferred height: minimum = %d\tnatural = %d", + self, *minimum_height, *natural_height); + } +} + + +static void +allocate_children (PhoshQuickSettingsBox *self, + int x, int y, int width, int height, + int cols, int rows, + int revealer_width, int revealer_height) +{ + int i = 0; + gboolean show_at_this_row = FALSE; + GtkAllocation rect; + gboolean ltr = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_LTR; + + rect.x = ltr ? x : x + (cols - 1) * (width + self->spacing); + rect.y = y; + + if (self->shown_child == NULL) { + rect.width = 0; + rect.height = 0; + gtk_widget_size_allocate (GTK_WIDGET (self->revealer), &rect); + } + + rect.width = width; + rect.height = height; + + for (int row = 0; row < rows; row++) { + for (int col = 0; col < cols; col++) { + while (i < self->children->len) { + PhoshQuickSetting *child = g_ptr_array_index (self->children, i); + i += 1; + if (gtk_widget_get_visible (GTK_WIDGET (child))) { + gtk_widget_size_allocate (GTK_WIDGET (child), &rect); + if (self->shown_child == child) + show_at_this_row = TRUE; + break; + } + } + if (ltr) + rect.x += width + self->spacing; + else + rect.x -= width + self->spacing; + } + rect.x = ltr ? x : x + (cols - 1) * (width + self->spacing); + rect.y += height + self->spacing; + + if (show_at_this_row) { + rect.x = x; + rect.height = revealer_height; + rect.width = revealer_width; + gtk_widget_size_allocate (GTK_WIDGET (self->revealer), &rect); + rect.x = ltr ? x : x + (cols - 1) * (width + self->spacing); + rect.height = height; + rect.width = width; + rect.y += revealer_height; + show_at_this_row = FALSE; + } + } +} + + +static void +phosh_quick_settings_box_size_allocate (GtkWidget *widget, GtkAllocation *allocation) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (widget); + int len = 0; + int child_width; + int child_height; + int cols; + int rows; + int total_width; + int total_height; + int extra_space; + int revealer_width = 0; + int revealer_height; + + g_debug ("%p: Doing size allocation", self); + + GTK_WIDGET_CLASS (phosh_quick_settings_box_parent_class)->size_allocate (widget, allocation); + + for (int i = 0; i < self->children->len; i++) + if (gtk_widget_get_visible (g_ptr_array_index (self->children, i))) + len += 1; + + if (len == 0) { + g_debug ("%p: Exiting allocation as there are no visible children", self); + return; + } + + g_debug ("%p: Allocation: x = %d\ty = %d\twidth = %d\theight = %d", + self, allocation->x, allocation->y, allocation->width, allocation->height); + + if (self->shown_child) { + gtk_widget_get_preferred_height_for_width (GTK_WIDGET (self->revealer), + allocation->width, NULL, + &revealer_height); + } else { + revealer_height = 0; + } + + child_width = compute_child_width (self); + child_height = compute_child_height (self); + + g_debug ("%p: Before: revealer width = %d\trevealer height = %d\tchild width = %d\t" + "child height = %d", + self, revealer_width, revealer_height, child_width, child_height); + + g_return_if_fail (allocation->width >= child_width && allocation->height >= child_height); + + cols = (int) floor ((float) (allocation->width + self->spacing) / (child_width + self->spacing)); + cols = MIN (cols, self->max_columns); + rows = (int) ceil ((float) len / cols); + g_debug ("%p: cols = %d\trows = %d", self, cols, rows); + + total_width = cols * (child_width + self->spacing) - self->spacing; + total_height = rows * (child_height + self->spacing) - self->spacing; + + if (total_width <= allocation->width && total_height <= allocation->height) { + extra_space = allocation->width - total_width; + child_width += extra_space / cols; + revealer_width = allocation->width; + + extra_space = allocation->height - total_height; + revealer_height = MIN (extra_space, revealer_height); + extra_space = allocation->height - total_height - revealer_height; + child_height += extra_space / rows; + } else { + cols = 1; + rows = len; + child_width = allocation->width; + child_height = (allocation->height - revealer_height + self->spacing) / rows - self->spacing; + revealer_width = allocation->width; + } + + g_return_if_fail (child_width >= 0 && child_height >= 0); + + g_debug ("%p: After: revealer width = %d\trevealer height = %d\tchild width = %d\t" + "child height = %d", + self, revealer_width, revealer_height, child_width, child_height); + + allocate_children (self, + allocation->x, allocation->y, child_width, child_height, + cols, rows, + revealer_width, revealer_height); +} + +static void +hide_status_page (PhoshQuickSettingsBox *self) +{ + PhoshStatusPage *status_page = phosh_quick_setting_get_status_page (self->shown_child); + gtk_container_remove (GTK_CONTAINER (self->revealer), GTK_WIDGET (status_page)); + phosh_quick_setting_set_showing_status (self->shown_child, FALSE); + self->shown_child = NULL; +} + + +static void +show_status_page (PhoshQuickSettingsBox *self) +{ + PhoshStatusPage *status_page = phosh_quick_setting_get_status_page (self->shown_child); + gtk_container_add (GTK_CONTAINER (self->revealer), GTK_WIDGET (status_page)); + phosh_quick_setting_set_showing_status (self->shown_child, TRUE); + gtk_revealer_set_reveal_child (self->revealer, TRUE); +} + + +static void +on_child_revealed_changed (PhoshQuickSettingsBox *self) +{ + gboolean child_revealed = gtk_revealer_get_child_revealed (self->revealer); + + if (child_revealed) + return; + + /* shown_child is NULL when the status-page was removed immediately */ + if (self->shown_child != NULL) + hide_status_page (self); + + if (self->to_show_child == NULL) + return; + + self->shown_child = self->to_show_child; + self->to_show_child = NULL; + show_status_page (self); +} + + +static void +on_hide_status (PhoshQuickSettingsBox *self, PhoshQuickSetting *child) +{ + if (child != self->shown_child) + return; + + gtk_revealer_set_reveal_child (self->revealer, FALSE); +} + + +static void +on_show_status (PhoshQuickSettingsBox *self, PhoshQuickSetting *child) +{ + if (self->shown_child != NULL) { + self->to_show_child = child; + gtk_revealer_set_reveal_child (self->revealer, FALSE); + } else { + self->shown_child = child; + show_status_page (self); + } +} + + +static void +on_status_page_changed (PhoshQuickSettingsBox *self, GParamSpec *pspec, PhoshQuickSetting *child) +{ + PhoshStatusPage *status_page = phosh_quick_setting_get_status_page (child); + + if (status_page) + gtk_widget_set_margin_bottom (GTK_WIDGET (status_page), self->spacing); + + if (child == self->shown_child) { + GtkWidget *existing_status = gtk_bin_get_child (GTK_BIN (self->revealer)); + gtk_container_remove (GTK_CONTAINER (self->revealer), existing_status); + + if (status_page == NULL) { + phosh_quick_setting_set_showing_status (self->shown_child, FALSE); + self->shown_child = NULL; + gtk_revealer_set_reveal_child (self->revealer, FALSE); + } else { + gtk_container_add (GTK_CONTAINER (self->revealer), GTK_WIDGET (status_page)); + } + } else if (child == self->to_show_child && status_page == NULL) { + self->to_show_child = NULL; + } +} + + +static void +depromote_child (PhoshQuickSettingsBox *self, PhoshQuickSetting *child) +{ + if (child == self->shown_child) { + hide_status_page (self); + gtk_revealer_set_reveal_child (self->revealer, FALSE); + } else if (child == self->to_show_child) { + self->to_show_child = NULL; + } +} + + +static void +on_sensitive_changed (PhoshQuickSettingsBox *self, GParamSpec *pspec, PhoshQuickSetting *child) +{ + gboolean sensitive = gtk_widget_get_sensitive (GTK_WIDGET (child)); + + if (sensitive) + return; + + depromote_child (self, child); +} + + +static void +on_visible_changed (PhoshQuickSettingsBox *self, GParamSpec *pspec, PhoshQuickSetting *child) +{ + gboolean visible = gtk_widget_get_visible (GTK_WIDGET (child)); + + if (visible) + return; + + depromote_child (self, child); +} + + +static void +container_add (GtkContainer *container, GtkWidget *widget) +{ + phosh_quick_settings_box_add (PHOSH_QUICK_SETTINGS_BOX (container), + PHOSH_QUICK_SETTING (widget)); +} + + +static void +container_remove (GtkContainer *container, GtkWidget *widget) +{ + phosh_quick_settings_box_remove (PHOSH_QUICK_SETTINGS_BOX (container), + PHOSH_QUICK_SETTING (widget)); +} + + +static void +phosh_quick_settings_box_forall (GtkContainer *container, gboolean include_internals, + GtkCallback callback, gpointer data) +{ + PhoshQuickSettingsBox *self = PHOSH_QUICK_SETTINGS_BOX (container); + + if (self->children != NULL) { + g_autoptr (GPtrArray) children = g_ptr_array_copy (self->children, NULL, NULL); + + for (int i = 0; i < children->len; i++) + callback (g_ptr_array_index (children, i), data); + } + + if (include_internals) + callback (GTK_WIDGET (self->revealer), data); +} + + +static void +phosh_quick_settings_box_class_init (PhoshQuickSettingsBoxClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->set_property = phosh_quick_settings_box_set_property; + object_class->get_property = phosh_quick_settings_box_get_property; + + widget_class->destroy = phosh_quick_settings_box_destroy; + widget_class->get_request_mode = phosh_quick_settings_box_get_request_mode; + widget_class->get_preferred_width = phosh_quick_settings_box_get_preferred_width; + widget_class->get_preferred_height_for_width = phosh_quick_settings_box_get_preferred_height_for_width; + widget_class->size_allocate = phosh_quick_settings_box_size_allocate; + + container_class->add = container_add; + container_class->remove = container_remove; + container_class->forall = phosh_quick_settings_box_forall; + + /** + * PhoshQuickSettingsBox:columns: + * + * The maximum and the preferred number of columns. + */ + props[PROP_MAX_COLUMNS] = + g_param_spec_uint ("max-columns", "", "", + 1, G_MAXUINT, 3, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSettingsBox:spacing: + * + * The spacing between the children. + */ + props[PROP_SPACING] = + g_param_spec_uint ("spacing", "", "", + 0, G_MAXUINT, 0, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshQuickSettingsBox:can-show-status: + * + * If the box can show status of its children. + */ + props[PROP_CAN_SHOW_STATUS] = + g_param_spec_boolean ("can-show-status", "", "", + TRUE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/quick-settings-box.ui"); + + gtk_widget_class_bind_template_callback (widget_class, on_child_revealed_changed); + gtk_widget_class_bind_template_child (widget_class, PhoshQuickSettingsBox, revealer); + + gtk_widget_class_set_css_name (widget_class, "phosh-quick-settings-box"); +} + + +static void +phosh_quick_settings_box_init (PhoshQuickSettingsBox *self) +{ + self->max_columns = 3; + self->can_show_status = TRUE; + self->children = g_ptr_array_new (); + + gtk_widget_init_template (GTK_WIDGET (self)); + gtk_widget_set_has_window (GTK_WIDGET (self), FALSE); + gtk_widget_set_parent (GTK_WIDGET (self->revealer), GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_quick_settings_box_new (guint max_columns, guint spacing) +{ + return g_object_new (PHOSH_TYPE_QUICK_SETTINGS_BOX, + "max-columns", max_columns, + "spacing", spacing, + NULL); +} + + +void +phosh_quick_settings_box_set_max_columns (PhoshQuickSettingsBox *self, guint max_columns) +{ + g_return_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self)); + + if (self->max_columns == max_columns) + return; + + self->max_columns = max_columns; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MAX_COLUMNS]); + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + + +guint +phosh_quick_settings_box_get_max_columns (PhoshQuickSettingsBox *self) +{ + g_return_val_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self), 0); + + return self->max_columns; +} + + +void +phosh_quick_settings_box_set_spacing (PhoshQuickSettingsBox *self, guint spacing) +{ + g_return_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self)); + + if (self->spacing == spacing) + return; + + self->spacing = spacing; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SPACING]); + + for (int i = 0; i < self->children->len; i++) { + PhoshQuickSetting *child = g_ptr_array_index (self->children, i); + PhoshStatusPage *status_page = phosh_quick_setting_get_status_page (child); + if (status_page) + gtk_widget_set_margin_bottom (GTK_WIDGET (status_page), self->spacing); + } + + gtk_widget_queue_resize (GTK_WIDGET (self)); +} + + +guint +phosh_quick_settings_box_get_spacing (PhoshQuickSettingsBox *self) +{ + g_return_val_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self), 0); + + return self->spacing; +} + + +void +phosh_quick_settings_box_set_can_show_status (PhoshQuickSettingsBox *self, gboolean can_show_status) +{ + g_return_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self)); + + if (self->can_show_status == can_show_status) + return; + + self->to_show_child = NULL; + if (self->shown_child != NULL && !can_show_status) + gtk_revealer_set_reveal_child (self->revealer, FALSE); + + self->can_show_status = can_show_status; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SHOW_STATUS]); +} + + +gboolean +phosh_quick_settings_box_get_can_show_status (PhoshQuickSettingsBox *self) +{ + g_return_val_if_fail (PHOSH_IS_QUICK_SETTINGS_BOX (self), FALSE); + + return self->can_show_status; +} + + +void +phosh_quick_settings_box_hide_status (PhoshQuickSettingsBox *self) +{ + g_return_if_fail (PHOSH_QUICK_SETTINGS_BOX (self)); + + on_hide_status (self, self->shown_child); +} + + +void +phosh_quick_settings_box_add (PhoshQuickSettingsBox *self, PhoshQuickSetting *child) +{ + g_return_if_fail (PHOSH_QUICK_SETTINGS_BOX (self)); + g_return_if_fail (PHOSH_IS_QUICK_SETTING (child)); + + g_ptr_array_add (self->children, child); + gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (self)); + + g_object_bind_property (self, + "can-show-status", + child, + "can-show-status", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + g_object_connect (child, + "swapped-object-signal::show-status", + G_CALLBACK (on_show_status), self, + "swapped-object-signal::hide-status", + G_CALLBACK (on_hide_status), self, + "swapped-object-signal::notify::status-page", + G_CALLBACK (on_status_page_changed), self, + "swapped-object-signal::notify::sensitive", + G_CALLBACK (on_sensitive_changed), self, + "swapped-object-signal::notify::visible", + G_CALLBACK (on_visible_changed), self, + NULL); + + on_status_page_changed (self, NULL, child); +} + + +void +phosh_quick_settings_box_remove (PhoshQuickSettingsBox *self, PhoshQuickSetting *child) +{ + g_return_if_fail (PHOSH_QUICK_SETTINGS_BOX (self)); + g_return_if_fail (PHOSH_IS_QUICK_SETTING (child)); + + if (child == self->shown_child) { + hide_status_page (self); + gtk_revealer_set_reveal_child (self->revealer, FALSE); + } else if (child == self->to_show_child) { + self->to_show_child = NULL; + } + + gtk_widget_unparent (GTK_WIDGET (child)); + g_ptr_array_remove (self->children, child); +} diff --git a/src/quick-settings-box.h b/src/quick-settings-box.h new file mode 100644 index 000000000..3c60064fb --- /dev/null +++ b/src/quick-settings-box.h @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "quick-setting.h" +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_QUICK_SETTINGS_BOX phosh_quick_settings_box_get_type () +G_DECLARE_FINAL_TYPE (PhoshQuickSettingsBox, phosh_quick_settings_box, PHOSH, QUICK_SETTINGS_BOX, GtkContainer) + +GtkWidget *phosh_quick_settings_box_new (guint max_columns, guint spacing); +void phosh_quick_settings_box_set_max_columns (PhoshQuickSettingsBox *self, guint max_columns); +guint phosh_quick_settings_box_get_max_columns (PhoshQuickSettingsBox *self); +void phosh_quick_settings_box_set_spacing (PhoshQuickSettingsBox *self, guint spacing); +guint phosh_quick_settings_box_get_spacing (PhoshQuickSettingsBox *self); +void phosh_quick_settings_box_set_can_show_status (PhoshQuickSettingsBox *self, gboolean can_show_status); +gboolean phosh_quick_settings_box_get_can_show_status (PhoshQuickSettingsBox *self); +void phosh_quick_settings_box_hide_status (PhoshQuickSettingsBox *self); +void phosh_quick_settings_box_add (PhoshQuickSettingsBox *self, PhoshQuickSetting *child); +void phosh_quick_settings_box_remove (PhoshQuickSettingsBox *self, PhoshQuickSetting *child); + +G_END_DECLS diff --git a/src/quick-settings.c b/src/quick-settings.c new file mode 100644 index 000000000..1d69dfdce --- /dev/null +++ b/src/quick-settings.c @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Arun Mani J + */ + +#define G_LOG_DOMAIN "phosh-quick-settings" + +#include "phosh-config.h" + +#include "quick-settings.h" + +#include "bt-status-page.h" +#include "feedback-status-page.h" +#include "plugin-loader.h" +#include "quick-setting.h" +#include "quick-settings-box.h" +#include "shell-priv.h" +#include "wifi-status-page.h" + +#define CUSTOM_QUICK_SETTINGS_SCHEMA "sm.puri.phosh.plugins" +#define CUSTOM_QUICK_SETTINGS_KEY "quick-settings" + +/** + * PhoshQuickSettings: + * + * A widget to display quick-settings using PhoshQuickSettingsBox. + * + * `PhoshQuickSettings` holds the default quick-settings and those loaded as plugins. It manages + * their interaction with user by launching appropriate actions. + * + * For example, tapping a Wi-Fi quick-setting would toggle its off/on state. Long pressing a + * rotation quick-setting would change the rotation configuration. + */ + +struct _PhoshQuickSettings { + GtkBin parent; + + PhoshQuickSettingsBox *box; + + GSettings *plugin_settings; + PhoshPluginLoader *plugin_loader; + GPtrArray *custom_quick_settings; +}; + +G_DEFINE_TYPE (PhoshQuickSettings, phosh_quick_settings, GTK_TYPE_BIN); + + +static void +on_wwan_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshWWan *wwan; + gboolean enabled; + + wwan = phosh_shell_get_wwan (shell); + g_return_if_fail (PHOSH_IS_WWAN (wwan)); + + enabled = phosh_wwan_is_enabled (wwan); + phosh_wwan_set_enabled (wwan, !enabled); +} + + +static void +on_wifi_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshWifiManager *manager; + gboolean enabled; + + manager = phosh_shell_get_wifi_manager (shell); + g_return_if_fail (PHOSH_IS_WIFI_MANAGER (manager)); + + enabled = phosh_wifi_manager_get_enabled (manager); + phosh_wifi_manager_set_enabled (manager, !enabled); +} + + +static void +on_bt_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshBtManager *manager; + gboolean enabled; + + manager = phosh_shell_get_bt_manager (shell); + g_return_if_fail (PHOSH_IS_BT_MANAGER (manager)); + + enabled = phosh_bt_manager_get_enabled (manager); + phosh_bt_manager_set_enabled (manager, !enabled); +} + + +static void +on_battery_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + GActionGroup *group; + + group = gtk_widget_get_action_group (GTK_WIDGET (self), "settings"); + g_return_if_fail (group); + g_action_group_activate_action (group, + "launch-panel", + g_variant_new ("(s@av)", "power", g_variant_new ("av", NULL))); +} + + +static void +on_rotate_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshRotationManager *rotation_manager; + PhoshRotationManagerMode mode; + PhoshMonitorTransform transform; + gboolean locked; + + rotation_manager = phosh_shell_get_rotation_manager (shell); + g_return_if_fail (rotation_manager); + mode = phosh_rotation_manager_get_mode (rotation_manager); + + switch (mode) { + case PHOSH_ROTATION_MANAGER_MODE_OFF: + transform = phosh_rotation_manager_get_transform (rotation_manager) ? + PHOSH_MONITOR_TRANSFORM_NORMAL : PHOSH_MONITOR_TRANSFORM_270; + phosh_rotation_manager_set_transform (rotation_manager, transform); + break; + case PHOSH_ROTATION_MANAGER_MODE_SENSOR: + locked = phosh_rotation_manager_get_orientation_locked (rotation_manager); + phosh_rotation_manager_set_orientation_locked (rotation_manager, !locked); + break; + default: + g_assert_not_reached (); + } +} + + +static void +on_rotate_long_pressed (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshRotationManager *rotation_manager; + PhoshRotationManagerMode mode; + + rotation_manager = phosh_shell_get_rotation_manager (shell); + g_return_if_fail (rotation_manager); + + mode = phosh_rotation_manager_get_mode (rotation_manager); + switch (mode) { + case PHOSH_ROTATION_MANAGER_MODE_OFF: + mode = PHOSH_ROTATION_MANAGER_MODE_SENSOR; + break; + case PHOSH_ROTATION_MANAGER_MODE_SENSOR: + mode = PHOSH_ROTATION_MANAGER_MODE_OFF; + break; + default: + g_assert_not_reached (); + } + g_debug ("Changing rotation mode to %d", mode); + phosh_rotation_manager_set_mode (rotation_manager, mode); +} + + +static void +on_feedback_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshFeedbackManager *manager; + + manager = phosh_shell_get_feedback_manager (shell); + g_return_if_fail (PHOSH_IS_FEEDBACK_MANAGER (manager)); + + phosh_feedback_manager_toggle (manager); +} + + +static void +on_torch_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshTorchManager *manager; + + manager = phosh_shell_get_torch_manager (shell); + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (manager)); + + phosh_torch_manager_toggle (manager); +} + + +static void +on_docked_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshDockedManager *manager; + gboolean enabled; + + manager = phosh_shell_get_docked_manager (shell); + g_return_if_fail (PHOSH_IS_DOCKED_MANAGER (manager)); + + enabled = phosh_docked_manager_get_enabled (manager); + phosh_docked_manager_set_enabled (manager, !enabled); +} + + +static void +on_vpn_clicked (PhoshQuickSettings *self, PhoshQuickSetting *child) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshVpnManager *vpn_manager; + + vpn_manager = phosh_shell_get_vpn_manager (shell); + g_return_if_fail (PHOSH_IS_VPN_MANAGER (vpn_manager)); + + phosh_vpn_manager_toggle_last_connection (vpn_manager); +} + + +static void +unload_custom_quick_setting (GtkWidget *quick_setting) +{ + PhoshQuickSettingsBox *box = PHOSH_QUICK_SETTINGS_BOX (gtk_widget_get_parent (quick_setting)); + phosh_quick_settings_box_remove (box, PHOSH_QUICK_SETTING (quick_setting)); +} + + +static void +load_custom_quick_settings (PhoshQuickSettings *self, GSettings *settings, char *key) +{ + g_auto (GStrv) plugins = NULL; + GtkWidget *widget; + + g_ptr_array_remove_range (self->custom_quick_settings, 0, self->custom_quick_settings->len); + plugins = g_settings_get_strv (self->plugin_settings, CUSTOM_QUICK_SETTINGS_KEY); + + if (plugins == NULL) + return; + + for (int i = 0; plugins[i]; i++) { + g_debug ("Loading custom quick setting: %s", plugins[i]); + widget = phosh_plugin_loader_load_plugin (self->plugin_loader, plugins[i]); + + if (widget == NULL) { + g_warning ("Custom quick setting '%s' not found", plugins[i]); + } else { + phosh_quick_settings_box_add (self->box, PHOSH_QUICK_SETTING (widget)); + g_ptr_array_add (self->custom_quick_settings, widget); + } + } +} + + +static void +phosh_quick_settings_dispose (GObject *object) +{ + PhoshQuickSettings *self = PHOSH_QUICK_SETTINGS (object); + + g_clear_object (&self->plugin_settings); + g_clear_object (&self->plugin_loader); + if (self->custom_quick_settings) { + g_ptr_array_free (self->custom_quick_settings, TRUE); + self->custom_quick_settings = NULL; + } + + G_OBJECT_CLASS (phosh_quick_settings_parent_class)->dispose (object); +} + + +static void +phosh_quick_settings_class_init (PhoshQuickSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_quick_settings_dispose; + + g_type_ensure (PHOSH_TYPE_QUICK_SETTINGS_BOX); + g_type_ensure (PHOSH_TYPE_QUICK_SETTING); + + g_type_ensure (PHOSH_TYPE_BT_STATUS_PAGE); + g_type_ensure (PHOSH_TYPE_FEEDBACK_STATUS_PAGE); + g_type_ensure (PHOSH_TYPE_WIFI_STATUS_PAGE); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/quick-settings.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshQuickSettings, box); + + gtk_widget_class_bind_template_callback (widget_class, on_wwan_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_wifi_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_bt_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_battery_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_rotate_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_rotate_long_pressed); + gtk_widget_class_bind_template_callback (widget_class, on_feedback_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_torch_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_docked_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_vpn_clicked); +} + + +static void +phosh_quick_settings_init (PhoshQuickSettings *self) +{ + const char *plugin_dirs[] = { PHOSH_PLUGINS_DIR, NULL}; + + gtk_widget_init_template (GTK_WIDGET (self)); + + g_object_bind_property (phosh_shell_get_default (), "locked", + self->box, "can-show-status", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + self->plugin_settings = g_settings_new (CUSTOM_QUICK_SETTINGS_SCHEMA); + self->plugin_loader = phosh_plugin_loader_new ((GStrv) plugin_dirs, + PHOSH_EXTENSION_POINT_QUICK_SETTING_WIDGET); + self->custom_quick_settings = g_ptr_array_new_with_free_func ((GDestroyNotify) unload_custom_quick_setting); + + g_signal_connect_object (self->plugin_settings, "changed::" CUSTOM_QUICK_SETTINGS_KEY, + G_CALLBACK (load_custom_quick_settings), self, G_CONNECT_SWAPPED); + + load_custom_quick_settings (self, NULL, NULL); +} + + +GtkWidget * +phosh_quick_settings_new (void) +{ + return g_object_new (PHOSH_TYPE_QUICK_SETTINGS, NULL); +} + + +void +phosh_quick_settings_hide_status (PhoshQuickSettings *self) +{ + g_return_if_fail (PHOSH_QUICK_SETTINGS (self)); + + phosh_quick_settings_box_hide_status (self->box); +} diff --git a/src/quick-settings.h b/src/quick-settings.h new file mode 100644 index 000000000..ef279b8d9 --- /dev/null +++ b/src/quick-settings.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_QUICK_SETTINGS phosh_quick_settings_get_type () +G_DECLARE_FINAL_TYPE (PhoshQuickSettings, phosh_quick_settings, PHOSH, QUICK_SETTINGS, GtkBin) + +GtkWidget *phosh_quick_settings_new (void); +void phosh_quick_settings_hide_status (PhoshQuickSettings *self); + +G_END_DECLS diff --git a/src/revealer.c b/src/revealer.c new file mode 100644 index 000000000..dd1328072 --- /dev/null +++ b/src/revealer.c @@ -0,0 +1,362 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-revealer" + +#include "phosh-config.h" + +#include "revealer.h" + +/** + * PhoshRevealer: + * + * Reveals e.g. a [class@StatusIcon] in the [class@TopPanel]. + * + * Similar to [class@Gtk.Revealer] but toggles the transition based on + * the `show-child` property which also triggers the child's + * visibility so it doesn't use up any size when not revealed + * (e.g. when using the `crossfade` animation). + * + * Since: 0.25.0 + */ + +enum { + PROP_0, + PROP_CHILD, + PROP_SHOW_CHILD, + PROP_TRANSITION_DURATION, + PROP_TRANSITION_TYPE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshRevealer { + GtkBin parent; + + GtkRevealer *revealer; + GtkWidget *child; + gboolean show_child; + guint transition_duration; + GtkRevealerTransitionType transition_type; +}; +G_DEFINE_TYPE (PhoshRevealer, phosh_revealer, GTK_TYPE_BIN) + + +static void +on_child_revealed_changed (PhoshRevealer *self) +{ + gboolean visible; + + visible = (gtk_revealer_get_child_revealed (GTK_REVEALER (self->revealer)) || + gtk_revealer_get_reveal_child (GTK_REVEALER (self->revealer))); + if (visible) + return; + + /* Hide the widget so it gives up it's space */ + if (self->child) + gtk_widget_set_visible (self->child, FALSE); +} + + +static void +phosh_revealer_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshRevealer *self = PHOSH_REVEALER (object); + + switch (property_id) { + case PROP_CHILD: + phosh_revealer_set_child (self, g_value_get_object (value)); + break; + case PROP_TRANSITION_DURATION: + phosh_revealer_set_transition_duration (self, g_value_get_uint (value)); + break; + case PROP_TRANSITION_TYPE: + phosh_revealer_set_transition_type (self, g_value_get_enum (value)); + break; + case PROP_SHOW_CHILD: + phosh_revealer_set_show_child (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_revealer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshRevealer *self = PHOSH_REVEALER (object); + + switch (property_id) { + case PROP_CHILD: + g_value_set_object (value, phosh_revealer_get_child (self)); + break; + case PROP_TRANSITION_DURATION: + g_value_set_uint (value, phosh_revealer_get_transition_duration (self)); + break; + case PROP_TRANSITION_TYPE: + g_value_set_enum (value, phosh_revealer_get_transition_type (self)); + break; + case PROP_SHOW_CHILD: + g_value_set_boolean (value, phosh_revealer_get_show_child (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_revealer_destroy (GtkWidget *widget) +{ + PhoshRevealer *self = PHOSH_REVEALER (widget); + + phosh_revealer_set_child (self, NULL); + + GTK_WIDGET_CLASS (phosh_revealer_parent_class)->destroy (widget); +} + + +static void +phosh_revealer_class_init (PhoshRevealerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_revealer_set_property; + object_class->get_property = phosh_revealer_get_property; + widget_class->destroy = phosh_revealer_destroy; + + /** + * PhoshRevealer:child: + * + * The child to be revealed and hidden. + */ + props[PROP_CHILD] = + g_param_spec_object ("child", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshRevealer:transition-duration: + * + * The duration of transition. + */ + props[PROP_TRANSITION_DURATION] = + g_param_spec_uint ("transition-duration", "", "", + 0, G_MAXUINT, 400, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshRevealer:transition-type: + * + * The type of transition. + */ + props[PROP_TRANSITION_TYPE] = + g_param_spec_enum ("transition-type", "", "", + GTK_TYPE_REVEALER_TRANSITION_TYPE, GTK_REVEALER_TRANSITION_TYPE_CROSSFADE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshRevealer:show-child: + * + * Whether the child should be shown. This make it visible and fades + * it in via the given transition. When %FALSE triggers the fade out + * animation and hides the child at the end. + */ + props[PROP_SHOW_CHILD] = + g_param_spec_boolean ("show-child", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/revealer.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshRevealer, revealer); + + gtk_widget_class_bind_template_callback (widget_class, on_child_revealed_changed); +} + + +static void +phosh_revealer_init (PhoshRevealer *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + self->transition_duration = 400; + self->transition_type = GTK_REVEALER_TRANSITION_TYPE_CROSSFADE; + on_child_revealed_changed (self); +} + + +PhoshRevealer * +phosh_revealer_new (void) +{ + return PHOSH_REVEALER (g_object_new (PHOSH_TYPE_REVEALER, NULL)); +} + +/** + * phosh_revealer_get_child: + * @self: The PhoshRevealer. + * + * Get the child of revealer. + * + * Returns:(transfer none): The child of revealer. + */ +GtkWidget * +phosh_revealer_get_child (PhoshRevealer *self) +{ + g_return_val_if_fail (PHOSH_IS_REVEALER (self), NULL); + + return self->child; +} + +/** + * phosh_revealer_set_child: + * @self: The PhoshRevealer. + * @child: The child to set. + * + * Set the child of revealer. Use `NULL` to remove existing child. + */ +void +phosh_revealer_set_child (PhoshRevealer *self, GtkWidget *child) +{ + g_return_if_fail (PHOSH_IS_REVEALER (self)); + g_return_if_fail (child == NULL || GTK_IS_WIDGET (child)); + + if (child == self->child) + return; + + if (self->child) + gtk_container_remove (GTK_CONTAINER (self->revealer), self->child); + + self->child = child; + + if (self->child) + gtk_container_add (GTK_CONTAINER (self->revealer), self->child); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]); +} + +/** + * phosh_revealer_get_show_child: + * @self: The PhoshRevealer: + * + * If %TRUE the child should be shown, otherwise hidden. + * + * Returns: Whether the child should be shown. + */ +gboolean +phosh_revealer_get_show_child (PhoshRevealer *self) +{ + g_return_val_if_fail (PHOSH_IS_REVEALER (self), FALSE); + + return self->show_child; +} + +/** + * phosh_revealer_set_show_child: + * @self: The PhoshRevealer: + * @show_child: Whether the child should be shown + * + * If `show_child` is %TRUE, the child will be set visible and shown + * using a %GtkRevealer Otherwise it will be hidden. + */ +void +phosh_revealer_set_show_child (PhoshRevealer *self, gboolean show_child) +{ + g_return_if_fail (PHOSH_IS_REVEALER (self)); + + if (show_child == self->show_child) + return; + + self->show_child = show_child; + if (show_child) { + if (self->child) + gtk_widget_set_visible (self->child, TRUE); + } else { + /* Child will be hidden at the end of the animation */ + } + + gtk_revealer_set_reveal_child (self->revealer, show_child); +} + +/** + * phosh_revealer_get_transition_duration: + * @self: The PhoshRevealer. + * + * Get the transition duration. + * + * Returns: The transition duration. + */ +guint +phosh_revealer_get_transition_duration (PhoshRevealer *self) +{ + g_return_val_if_fail (PHOSH_IS_REVEALER (self), 0); + + return self->transition_duration; +} + +/** + * phosh_revealer_set_transition_duration: + * @self: The PhoshRevealer. + * @transition_duraiton: Duration for transition. + * + * Set the transition duration. + */ +void +phosh_revealer_set_transition_duration (PhoshRevealer *self, guint transition_duration) +{ + g_return_if_fail (PHOSH_IS_REVEALER (self)); + + if (self->transition_duration == transition_duration) + return; + + self->transition_duration = transition_duration; + gtk_revealer_set_transition_duration (self->revealer, self->transition_duration); +} + +/** + * phosh_revealer_get_transition_type: + * @self: The PhoshRevealer. + * + * Get the transition type. + * + * Returns: The transition type. + */ +GtkRevealerTransitionType +phosh_revealer_get_transition_type (PhoshRevealer *self) +{ + g_return_val_if_fail (PHOSH_IS_REVEALER (self), GTK_REVEALER_TRANSITION_TYPE_NONE); + + return self->transition_type; +} + +/** + * phosh_revealer_set_transition_type: + * @self: The PhoshRevealer. + * @transition_type: Type for transition. + * + * Set the transition type. + */ +void +phosh_revealer_set_transition_type (PhoshRevealer *self, GtkRevealerTransitionType transition_type) +{ + g_return_if_fail (PHOSH_IS_REVEALER (self)); + + if (self->transition_type == transition_type) + return; + + self->transition_type = transition_type; + gtk_revealer_set_transition_type (self->revealer, self->transition_type); +} diff --git a/src/revealer.h b/src/revealer.h new file mode 100644 index 000000000..e88df1a8f --- /dev/null +++ b/src/revealer.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_REVEALER (phosh_revealer_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshRevealer, phosh_revealer, PHOSH, REVEALER, GtkBin) + +PhoshRevealer * phosh_revealer_new (void); +GtkWidget * phosh_revealer_get_child (PhoshRevealer *self); +void phosh_revealer_set_child (PhoshRevealer *self, GtkWidget *child); +gboolean phosh_revealer_get_show_child (PhoshRevealer *self); +void phosh_revealer_set_show_child (PhoshRevealer *self, gboolean show_child); +guint phosh_revealer_get_transition_duration (PhoshRevealer *self); +void phosh_revealer_set_transition_duration (PhoshRevealer *self, + guint transition_duration); +GtkRevealerTransitionType phosh_revealer_get_transition_type (PhoshRevealer *self); +void phosh_revealer_set_transition_type (PhoshRevealer *self, + GtkRevealerTransitionType + transition_type); + +G_END_DECLS diff --git a/src/rotateinfo.c b/src/rotateinfo.c new file mode 100644 index 000000000..7d0f3a178 --- /dev/null +++ b/src/rotateinfo.c @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Julian Sparber + */ + +#define G_LOG_DOMAIN "phosh-rotateinfo" + +#include "phosh-config.h" + +#include "rotateinfo.h" +#include "shell-priv.h" + +/** + * PhoshRotateInfo: + * + * A widget to display the rotate lock status + * + * A #PhoshStatusIcon to display the rotation lock status. + * It can either display whether a rotation lock is currently active or + * if the output is in portrait/landscape mode. + */ + +enum { + PROP_0, + PROP_ENABLED, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshRotateInfo { + PhoshStatusIcon parent; + + PhoshRotationManager *manager; + gboolean present; + gboolean enabled; +} PhoshRotateInfo; + + +G_DEFINE_TYPE (PhoshRotateInfo, phosh_rotate_info, PHOSH_TYPE_STATUS_ICON) + + +static void +phosh_rotation_info_check_enabled (PhoshRotateInfo *self) +{ + gboolean enabled = FALSE; + + if ((phosh_rotation_manager_get_mode (self->manager) == PHOSH_ROTATION_MANAGER_MODE_SENSOR) && + phosh_rotation_manager_get_orientation_locked (self->manager) == FALSE) { + enabled = TRUE; + } + + if (self->enabled == enabled) + return; + + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + + +static void +on_transform_changed (PhoshRotateInfo *self) +{ + PhoshMonitor *monitor = phosh_rotation_manager_get_monitor (self->manager); + gboolean monitor_is_landscape; + gboolean portrait; + + if (phosh_rotation_manager_get_mode (self->manager) != PHOSH_ROTATION_MANAGER_MODE_OFF) + return; + + if (!monitor) + return; + + switch (phosh_rotation_manager_get_transform (self->manager)) { + case PHOSH_MONITOR_TRANSFORM_NORMAL: + case PHOSH_MONITOR_TRANSFORM_FLIPPED: + case PHOSH_MONITOR_TRANSFORM_180: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_180: + portrait = TRUE; + break; + case PHOSH_MONITOR_TRANSFORM_90: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_90: + case PHOSH_MONITOR_TRANSFORM_270: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_270: + portrait = FALSE; + break; + default: + g_warn_if_reached(); + portrait = TRUE; + } + + /* If we have a landscape monitor (tv, laptop) flip the rotation */ + monitor_is_landscape = ((double)monitor->width / (double)monitor->height) > 1.0; + portrait = monitor_is_landscape ? !portrait : portrait; + + g_debug ("Portrait: %d, width: %d, height: %d", portrait, monitor->width, monitor->height); + if (portrait) { + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), "screen-rotation-portrait-symbolic"); + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Portrait")); + } else { + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), "screen-rotation-landscape-symbolic"); + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Landscape")); + } +} + + +static void +on_orientation_lock_changed (PhoshRotateInfo *self) +{ + gboolean locked = phosh_rotation_manager_get_orientation_locked (self->manager); + const char *icon_name; + + if (phosh_rotation_manager_get_mode (self->manager) != PHOSH_ROTATION_MANAGER_MODE_SENSOR) + return; + + g_debug ("Orientation locked: %d", locked); + + icon_name = locked ? "rotation-locked-symbolic" : "rotation-allowed-symbolic"; + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), icon_name); + /* Translators: Automatic screen orientation is either on (enabled) or off (locked/disabled) */ + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), locked ? + C_("automatic-screen-rotation-disabled", "Off") : + C_("automatic-screen-rotation-enabled", "On")); + phosh_rotation_info_check_enabled (self); + + return; +} + + +static void +on_mode_or_monitor_changed (PhoshRotateInfo *self) +{ + PhoshRotationManagerMode mode = phosh_rotation_manager_get_mode (self->manager); + gboolean present = !!phosh_rotation_manager_get_monitor (self->manager); + + g_debug ("Rotation manager mode: %d, has-builtin: %d", mode, present); + switch (mode) { + case PHOSH_ROTATION_MANAGER_MODE_OFF: + on_transform_changed (self); + break; + case PHOSH_ROTATION_MANAGER_MODE_SENSOR: + on_orientation_lock_changed (self); + break; + default: + g_assert_not_reached (); + } + + phosh_rotation_info_check_enabled (self); + if (self->present == present) + return; + + self->present = present; + g_debug ("Built-in monitor present: %d", present); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_rotate_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshRotateInfo *self = PHOSH_ROTATE_INFO (object); + + switch (property_id) { + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_rotate_info_class_init (PhoshRotateInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_rotate_info_get_property; + + gtk_widget_class_set_css_name (widget_class, "phosh-rotate-info"); + + /** + * PhoshRotateInfo:present: + * + * Whether a builtin display to rotate is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + /** + * PhoshRotateInfo:enabled: + * + * Whether automatic rotation is enabled + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_rotate_info_init (PhoshRotateInfo *self) +{ + self->manager = phosh_shell_get_rotation_manager (phosh_shell_get_default()); + + phosh_status_icon_set_icon_name (PHOSH_STATUS_ICON (self), "rotation-locked-symbolic"); + /* Translators: Automatic screen orientation is off (locked/disabled) */ + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), + C_("automatic-screen-rotation-disabled", "Off")); + + /* We don't use property bindings since we flip info/icon based on rotation and lock */ + g_signal_connect_object (self->manager, + "notify::transform", + G_CALLBACK (on_transform_changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->manager, + "notify::orientation-locked", + G_CALLBACK (on_orientation_lock_changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->manager, + "notify::mode", + G_CALLBACK (on_mode_or_monitor_changed), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->manager, + "notify::monitor", + G_CALLBACK (on_mode_or_monitor_changed), + self, + G_CONNECT_SWAPPED); + + on_mode_or_monitor_changed (self); +} diff --git a/src/rotateinfo.h b/src/rotateinfo.h new file mode 100644 index 000000000..bff6eee5b --- /dev/null +++ b/src/rotateinfo.h @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +/** + * PhoshRotateInfoMode: + * @PHOSH_ROTATE_INFO_MODE_LOCK: Button toggles rotation lock + * @PHOSH_ROTATE_INFO_MODE_TOGGLE: Button toggles portrait/landscape + * + * What is toggled when short pressing the rotation info quick setting + */ +typedef enum { + PHOSH_ROTATE_INFO_MODE_LOCK, + PHOSH_ROTATE_INFO_MODE_TOGGLE, +} PhoshRotateInfoMode; + +#define PHOSH_TYPE_ROTATE_INFO (phosh_rotate_info_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshRotateInfo, phosh_rotate_info, PHOSH, ROTATE_INFO, PhoshStatusIcon) + +GtkWidget *phosh_rotate_info_new (void); +PhoshRotateInfoMode phosh_rotate_info_get_mode (PhoshRotateInfo *self); +void phosh_rotate_info_set_mode (PhoshRotateInfo *self, PhoshRotateInfoMode mode); + +G_END_DECLS diff --git a/src/rotation-manager.c b/src/rotation-manager.c new file mode 100644 index 000000000..fdd37d052 --- /dev/null +++ b/src/rotation-manager.c @@ -0,0 +1,754 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-rotation-manager" + +#include "phosh-config.h" +#include "rotation-manager.h" +#include "shell-priv.h" +#include "sensor-proxy-manager.h" +#include "util.h" + +#define ORIENTATION_LOCK_SCHEMA_ID "org.gnome.settings-daemon.peripherals.touchscreen" +#define ORIENTATION_LOCK_KEY "orientation-lock" + +/** + * PhoshRotationManager: + * + * The Rotation Manager + * + * #PhoshRotationManager is responsible for managing the rotation of + * a given #PhoshMonitor. Depending on the #PhoshRotationManagerMode + * this can happen by interfacing with a #PhoshSensorProxyManager or + * by setting the #PhoshMonitorTransform explicitly. + * It also takes the #PhoshLockscreenManager:locked status into account + * to ensure the lockscreen is rotated accordingly on small phones. + */ + +enum { + PROP_0, + PROP_SENSOR_PROXY_MANAGER, + PROP_LOCKSCREEN_MANAGER, + PROP_ORIENTATION_LOCKED, + PROP_MONITOR, + PROP_MODE, + PROP_TRANSFORM, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + +typedef struct _PhoshRotationManager { + GObject parent; + + gboolean claimed; + GCancellable *cancel; + PhoshSensorProxyManager *sensor_proxy_manager; + PhoshLockscreenManager *lockscreen_manager; + PhoshMonitor *monitor; + PhoshMonitorTransform transform; + PhoshMonitorTransform prelock_transform; + gboolean blanked; + + GSettings *settings; + gboolean orientation_locked; + + PhoshRotationManagerMode mode; +} PhoshRotationManager; + +G_DEFINE_TYPE (PhoshRotationManager, phosh_rotation_manager, G_TYPE_OBJECT); + + +static void +apply_transform (PhoshRotationManager *self, PhoshMonitorTransform transform) +{ + PhoshMonitorTransform current; + PhoshMonitorManager *monitor_manager = phosh_shell_get_monitor_manager (phosh_shell_get_default()); + + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (monitor_manager)); + + if (!self->monitor) + return; + + current = phosh_monitor_get_transform (self->monitor); + if (current == transform) + return; + + g_debug ("Rotating %s to %d", self->monitor->name, transform); + phosh_monitor_manager_set_monitor_transform (monitor_manager, + self->monitor, + transform); + phosh_monitor_manager_apply_monitor_config (monitor_manager); +} + +/** + * match_orientation: + * @self: The #PhoshRotationManager + * + * Match the screen orientation to the sensor output. + * Do nothing if orientation lock is on or there's no + * sensor claimed. + * + * Returns: %TRUE if the orientation was matched, otherwise %FALSE. + */ +static gboolean +match_orientation (PhoshRotationManager *self) +{ + const char *orient; + PhoshMonitorTransform transform; + + if (self->orientation_locked || !self->claimed || + phosh_lockscreen_manager_get_locked (self->lockscreen_manager) || + self->mode == PHOSH_ROTATION_MANAGER_MODE_OFF) + return FALSE; + + orient = phosh_dbus_sensor_proxy_get_accelerometer_orientation ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + g_debug ("Orientation changed: %s", orient); + + if (!g_strcmp0 ("normal", orient)) { + transform = PHOSH_MONITOR_TRANSFORM_NORMAL; + } else if (!g_strcmp0 ("right-up", orient)) { + transform = PHOSH_MONITOR_TRANSFORM_270; + } else if (!g_strcmp0 ("bottom-up", orient)) { + transform = PHOSH_MONITOR_TRANSFORM_180; + } else if (!g_strcmp0 ("left-up", orient)) { + transform = PHOSH_MONITOR_TRANSFORM_90; + } else if (!g_strcmp0 ("undefined", orient)) { + return FALSE; /* just leave as is */ + } else { + g_warning ("Unknown orientation '%s'", orient); + return FALSE; + } + + apply_transform (self, transform); + return TRUE; +} + +static void +on_accelerometer_claimed (PhoshSensorProxyManager *sensor_proxy_manager, + GAsyncResult *res, + PhoshRotationManager *self) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_claim_accelerometer_finish ( + PHOSH_DBUS_SENSOR_PROXY (sensor_proxy_manager), + res, &err); + + if (!success) { + phosh_async_error_warn (err, "Failed to claim accelerometer"); + return; + } + + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (sensor_proxy_manager == self->sensor_proxy_manager); + + g_debug ("Claimed accelerometer"); + self->claimed = TRUE; + match_orientation (self); +} + +static void +on_accelerometer_released (PhoshSensorProxyManager *sensor_proxy_manager, + GAsyncResult *res, + PhoshRotationManager *self) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + g_return_if_fail (PHOSH_IS_SENSOR_PROXY_MANAGER (sensor_proxy_manager)); + + success = phosh_dbus_sensor_proxy_call_release_accelerometer_finish ( + PHOSH_DBUS_SENSOR_PROXY (sensor_proxy_manager), + res, &err); + + if (!success) { + phosh_async_error_warn (err, "Failed to release accelerometer cleanly"); + return; + } + + g_debug ("Released accelerometer"); +} + +static void +phosh_rotation_manager_claim_accelerometer (PhoshRotationManager *self, gboolean claim) +{ + if (claim == self->claimed) + return; + + if (!self->sensor_proxy_manager) + return; + + g_debug ("Claiming accelerometer: %d", claim); + if (claim) { + phosh_dbus_sensor_proxy_call_claim_accelerometer ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), + self->cancel, + (GAsyncReadyCallback)on_accelerometer_claimed, + self); + } else { + phosh_dbus_sensor_proxy_call_release_accelerometer ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), + self->cancel, + (GAsyncReadyCallback)on_accelerometer_released, + self); + /* Immediately mark the sensor as released */ + self->claimed = FALSE; + } +} + +static void +on_has_accelerometer_changed (PhoshRotationManager *self, + GParamSpec *pspec, + PhoshSensorProxyManager *proxy) +{ + gboolean has_accel; + PhoshRotationManagerMode mode; + + has_accel = phosh_dbus_sensor_proxy_get_has_accelerometer ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + g_debug ("Found %s accelerometer", has_accel ? "a" : "no"); + + mode = has_accel ? PHOSH_ROTATION_MANAGER_MODE_SENSOR : PHOSH_ROTATION_MANAGER_MODE_OFF; + phosh_rotation_manager_set_mode (self, mode); +} + +/** + * fixup_lockscreen_orientation: + * @self: The PhoshRotationManager + * @lock: %True if the screen is being locked, %FALSE for unlock + * + * On phones the lock screen doesn't work in landscape so fix that up + * by rotating to portrait on lock and back to the old orientation on + * unlock. + * + * See https://source.puri.sm/Librem5/phosh/-/issues/388 + * + * Keep all of this local to this function. + */ +static void +fixup_lockscreen_orientation (PhoshRotationManager *self, gboolean lock) +{ + PhoshShell *shell = phosh_shell_get_default (); + PhoshModeManager *mode_manager = phosh_shell_get_mode_manager(shell); + PhoshMonitorTransform transform; + + g_return_if_fail (PHOSH_IS_MODE_MANAGER (mode_manager)); + + if (!self->monitor) + return; + + /* Only bother on phones */ + if (phosh_mode_manager_get_device_type(mode_manager) != PHOSH_MODE_DEVICE_TYPE_PHONE && + phosh_mode_manager_get_device_type(mode_manager) != PHOSH_MODE_DEVICE_TYPE_UNKNOWN) + return; + + /* Don't mess with transforms on external screens either */ + if (!phosh_monitor_is_builtin (self->monitor)) + return; + + if (lock) { + if (self->prelock_transform == -1) { + self->prelock_transform = phosh_monitor_get_transform (self->monitor); + g_debug ("Saving prelock transform %d", self->prelock_transform); + } + + /* Use prelock transform if portrait, else use normal */ + transform = (self->prelock_transform % 2) == 0 ? self->prelock_transform : + PHOSH_MONITOR_TRANSFORM_NORMAL; + g_debug ("Forcing portrait transform: %d", transform); + } else { + if (self->prelock_transform == -1) { + g_warning ("Prelock transform invalid"); + self->prelock_transform = PHOSH_MONITOR_TRANSFORM_NORMAL; + } + g_debug ("Restoring transform %d", self->prelock_transform); + transform = self->prelock_transform; + self->prelock_transform = -1; + } + + apply_transform (self, transform); +} + + +static void +claim_or_release_accelerometer (PhoshRotationManager *self) +{ + gboolean claim = TRUE; + + /* No need for accel on screen blank, saves power */ + if (phosh_shell_get_state (phosh_shell_get_default ()) & PHOSH_STATE_BLANKED) + claim = FALSE; + + /* No need for accel on orientation lock, saves power */ + if (self->orientation_locked) + claim = FALSE; + + /* No need for accel when automatic rotation is not requested or possible */ + if (self->mode == PHOSH_ROTATION_MANAGER_MODE_OFF) + claim = FALSE; + + if (claim == self->claimed) + return; + + phosh_rotation_manager_claim_accelerometer (self, claim); +} + + +static void +on_shell_state_changed (PhoshRotationManager *self, + GParamSpec *pspec, + PhoshShell *shell) +{ + PhoshShellStateFlags state; + gboolean blanked; + + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SHELL (shell)); + + state = phosh_shell_get_state (shell); + g_debug ("Shell state changed: %d", state); + + blanked = !!(phosh_shell_get_state (phosh_shell_get_default ()) & PHOSH_STATE_BLANKED); + + /* We're only interested in blank state changed */ + if (blanked == self->blanked) + return; + self->blanked = blanked; + + /* Claim/unclaim sensor if blank state changed */ + claim_or_release_accelerometer (self); + + if (blanked) + return; + + /* Fixup lockscreen orientation on unblank */ + if (!blanked && phosh_lockscreen_manager_get_locked (self->lockscreen_manager)) { + fixup_lockscreen_orientation (self, TRUE); + } +} + + +static void +on_lockscreen_manager_locked (PhoshRotationManager *self, GParamSpec *pspec, + PhoshLockscreenManager *lockscreen_manager) +{ + gboolean locked; + + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (lockscreen_manager)); + + locked = phosh_lockscreen_manager_get_locked (self->lockscreen_manager); + + if (locked) { + fixup_lockscreen_orientation (self, TRUE); + } else { + gboolean matched = match_orientation (self); + + /* If we couldn't match the orientation (either because it was inhibited or it failed) + use the last known transform */ + if (!matched) + fixup_lockscreen_orientation (self, FALSE); + } +} + + +static void +on_accelerometer_orientation_changed (PhoshRotationManager *self, + GParamSpec *pspec, + PhoshSensorProxyManager *sensor) +{ + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (self->sensor_proxy_manager == sensor); + + match_orientation (self); +} + + +static void +on_monitor_configured (PhoshRotationManager *self, + PhoshMonitor *monitor) +{ + PhoshMonitorTransform transform; + + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + transform = phosh_monitor_get_transform (monitor); + if (transform == self->transform) + return; + + self->transform = transform; + g_debug ("Rotation-manager transform %d", transform); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TRANSFORM]); +} + + +static void +phosh_rotation_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshRotationManager *self = PHOSH_ROTATION_MANAGER (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + /* construct only */ + self->sensor_proxy_manager = g_value_dup_object (value); + break; + case PROP_LOCKSCREEN_MANAGER: + /* construct only */ + self->lockscreen_manager = g_value_dup_object (value); + break; + case PROP_MONITOR: + phosh_rotation_manager_set_monitor (self, g_value_get_object (value)); + break; + case PROP_ORIENTATION_LOCKED: + phosh_rotation_manager_set_orientation_locked (self, + g_value_get_boolean (value)); + break; + case PROP_MODE: + phosh_rotation_manager_set_mode (self, g_value_get_enum (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_rotation_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshRotationManager *self = PHOSH_ROTATION_MANAGER (object); + + switch (property_id) { + case PROP_SENSOR_PROXY_MANAGER: + g_value_set_object (value, self->sensor_proxy_manager); + break; + case PROP_LOCKSCREEN_MANAGER: + g_value_set_object (value, self->lockscreen_manager); + break; + case PROP_ORIENTATION_LOCKED: + g_value_set_boolean (value, self->orientation_locked); + break; + case PROP_MODE: + g_value_set_enum (value, self->mode); + break; + case PROP_TRANSFORM: + g_value_set_enum (value, self->transform); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +phosh_rotation_manager_constructed (GObject *object) +{ + PhoshRotationManager *self = PHOSH_ROTATION_MANAGER (object); + + G_OBJECT_CLASS (phosh_rotation_manager_parent_class)->constructed (object); + + self->settings = g_settings_new (ORIENTATION_LOCK_SCHEMA_ID); + + g_settings_bind (self->settings, + ORIENTATION_LOCK_KEY, + self, + "orientation-locked", + G_BINDING_SYNC_CREATE + | G_BINDING_BIDIRECTIONAL); + + g_signal_connect_swapped (self->lockscreen_manager, + "notify::locked", + (GCallback) on_lockscreen_manager_locked, + self); + on_lockscreen_manager_locked (self, NULL, self->lockscreen_manager); + + self->blanked = !!(phosh_shell_get_state (phosh_shell_get_default ()) & PHOSH_STATE_BLANKED); + g_signal_connect_object (phosh_shell_get_default (), + "notify::shell-state", + G_CALLBACK (on_shell_state_changed), + self, + G_CONNECT_SWAPPED); + + if (!self->sensor_proxy_manager) { + g_message ("Got no sensor-proxy, no automatic rotation"); + return; + } + + g_signal_connect_swapped (self->sensor_proxy_manager, + "notify::accelerometer-orientation", + (GCallback) on_accelerometer_orientation_changed, + self); + + g_signal_connect_swapped (self->sensor_proxy_manager, + "notify::has-accelerometer", + (GCallback) on_has_accelerometer_changed, + self); + on_has_accelerometer_changed (self, NULL, self->sensor_proxy_manager); +} + + +static void +phosh_rotation_manager_dispose (GObject *object) +{ + PhoshRotationManager *self = PHOSH_ROTATION_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->settings); + + if (self->sensor_proxy_manager) { + g_signal_handlers_disconnect_by_data (self->sensor_proxy_manager, + self); + /* Sync call since we're going away */ + phosh_dbus_sensor_proxy_call_release_accelerometer_sync ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager), NULL, NULL); + g_clear_object (&self->sensor_proxy_manager); + } + + if (self->lockscreen_manager) { + g_signal_handlers_disconnect_by_data (self->lockscreen_manager, + self); + g_clear_object (&self->lockscreen_manager); + } + + if (self->monitor) { + g_signal_handlers_disconnect_by_data (self->monitor, + self); + g_clear_object (&self->monitor); + } + + G_OBJECT_CLASS (phosh_rotation_manager_parent_class)->dispose (object); +} + +static void +phosh_rotation_manager_class_init (PhoshRotationManagerClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + + object_class->constructed = phosh_rotation_manager_constructed; + object_class->dispose = phosh_rotation_manager_dispose; + + object_class->set_property = phosh_rotation_manager_set_property; + object_class->get_property = phosh_rotation_manager_get_property; + + props[PROP_SENSOR_PROXY_MANAGER] = + g_param_spec_object ( + "sensor-proxy-manager", + "Sensor proxy manager", + "The object inerfacing with iio-sensor-proxy", + PHOSH_TYPE_SENSOR_PROXY_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_LOCKSCREEN_MANAGER] = + g_param_spec_object ( + "lockscreen-manager", + "Lockscren manager", + "The object managing the lock screen", + PHOSH_TYPE_LOCKSCREEN_MANAGER, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_MONITOR] = + g_param_spec_object ( + "monitor", + "Monitor", + "The monitor to rotate", + PHOSH_TYPE_MONITOR, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + props[PROP_ORIENTATION_LOCKED] = + g_param_spec_boolean ( + "orientation-locked", + "Screen orientation locked", + "Whether the screen orientation is locked", + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_MODE] = + g_param_spec_enum ( + "mode", + "Rotation mode", + "The current rotation mode", + PHOSH_TYPE_ROTATION_MANAGER_MODE, + PHOSH_ROTATION_MANAGER_MODE_OFF, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + props[PROP_TRANSFORM] = + g_param_spec_enum ("transform", + "Transform", + "Monitor transform of the rotation monitor", + PHOSH_TYPE_MONITOR_TRANSFORM, + PHOSH_MONITOR_TRANSFORM_NORMAL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, LAST_PROP, props); +} + +static void +phosh_rotation_manager_init (PhoshRotationManager *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshRotationManager * +phosh_rotation_manager_new (PhoshSensorProxyManager *sensor_proxy_manager, + PhoshLockscreenManager *lockscreen_manager, + PhoshMonitor *monitor) +{ + return g_object_new (PHOSH_TYPE_ROTATION_MANAGER, + "sensor-proxy-manager", sensor_proxy_manager, + "lockscreen-manager", lockscreen_manager, + "monitor", monitor, + NULL); +} + +void +phosh_rotation_manager_set_orientation_locked (PhoshRotationManager *self, gboolean locked) +{ + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + + if (locked == self->orientation_locked) + return; + + self->orientation_locked = locked; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ORIENTATION_LOCKED]); + + /* We don't need accel if orientation locked and vice versa */ + if (self->claimed == self->orientation_locked) + claim_or_release_accelerometer (self); + else + match_orientation (self); +} + +gboolean +phosh_rotation_manager_get_orientation_locked (PhoshRotationManager *self) +{ + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (self), TRUE); + + return self->orientation_locked; +} + +PhoshRotationManagerMode +phosh_rotation_manager_get_mode (PhoshRotationManager *self) +{ + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (self), PHOSH_ROTATION_MANAGER_MODE_OFF); + + return self->mode; +} + +/** + * phosh_rotation_manager_set_mode: + * @self: The #PhoshRotationManager + * @mode: The #PhoshRotationManagerMode to set + * + * Sets the given mode. + * Returns: %TRUE if setting the mode was possible, otherwise %FALSE (e.g. when trying + * to set %PHOSH_ROTATION_MANAGER_MODE_SENSOR without having a sensor. + */ +gboolean +phosh_rotation_manager_set_mode (PhoshRotationManager *self, PhoshRotationManagerMode mode) +{ + gboolean has_accel; + + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (self), FALSE); + + if (mode == self->mode) + return TRUE; + + has_accel = phosh_dbus_sensor_proxy_get_has_accelerometer ( + PHOSH_DBUS_SENSOR_PROXY (self->sensor_proxy_manager)); + + if (mode == PHOSH_ROTATION_MANAGER_MODE_SENSOR && !has_accel) + return FALSE; + + self->mode = mode; + + g_debug ("Setting mode: %d", mode); + claim_or_release_accelerometer (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MODE]); + return TRUE; +} + + +void +phosh_rotation_manager_set_transform (PhoshRotationManager *self, + PhoshMonitorTransform transform) +{ + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (self->mode == PHOSH_ROTATION_MANAGER_MODE_OFF); + + apply_transform (self, transform); +} + +PhoshMonitorTransform +phosh_rotation_manager_get_transform (PhoshRotationManager *self) +{ + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (self), PHOSH_MONITOR_TRANSFORM_NORMAL); + g_return_val_if_fail (PHOSH_IS_MONITOR (self->monitor), PHOSH_MONITOR_TRANSFORM_NORMAL); + + return self->monitor->transform; +} + +/** + * phosh_rotation_manager_get_monitor: + * @self: The PhoshRotationManager + * + * Get the monitor this manager currently acts on + * + * Returns:(transfer none): The current monitor + */ +PhoshMonitor * +phosh_rotation_manager_get_monitor (PhoshRotationManager *self) +{ + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (self), NULL); + + return self->monitor; +} + + +void +phosh_rotation_manager_set_monitor (PhoshRotationManager *self, PhoshMonitor *monitor) +{ + g_return_if_fail (PHOSH_IS_ROTATION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor) || monitor == NULL); + + g_debug ("Using monitor %p", monitor); + + if (self->monitor == monitor) + return; + + if (self->monitor) { + g_signal_handlers_disconnect_by_data (self->monitor, self); + g_clear_object (&self->monitor); + } + + if (monitor == NULL) { + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MONITOR]); + return; + } + + self->monitor = g_object_ref (monitor); + g_signal_connect_swapped (self->monitor, + "configured", + G_CALLBACK (on_monitor_configured), + self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MONITOR]); +} diff --git a/src/rotation-manager.h b/src/rotation-manager.h new file mode 100644 index 000000000..53ca1e320 --- /dev/null +++ b/src/rotation-manager.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ +#pragma once + +#include "lockscreen-manager.h" +#include "sensor-proxy-manager.h" +#include "monitor/monitor.h" + +G_BEGIN_DECLS + +/** + * PhoshRotationManagerMode: + * @PHOSH_ROTATION_MANAGER_MODE_OFF: automatic rotation off + * @PHOSH_ROTATION_MANAGER_MODE_SENSOR: rotation driven by sensor orientation + * + * The mode of a #PhoshRotationManager + */ +typedef enum { + PHOSH_ROTATION_MANAGER_MODE_OFF, + PHOSH_ROTATION_MANAGER_MODE_SENSOR, +} PhoshRotationManagerMode; + +#define PHOSH_TYPE_ROTATION_MANAGER (phosh_rotation_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshRotationManager, phosh_rotation_manager, PHOSH, ROTATION_MANAGER, GObject); + +PhoshRotationManager *phosh_rotation_manager_new (PhoshSensorProxyManager *sensor_proxy_manager, + PhoshLockscreenManager *lockscreen_manager, + PhoshMonitor *monitor); +void phosh_rotation_manager_set_orientation_locked (PhoshRotationManager *self, + gboolean locked); +gboolean phosh_rotation_manager_get_orientation_locked (PhoshRotationManager *self); + +PhoshRotationManagerMode phosh_rotation_manager_get_mode (PhoshRotationManager *self); +gboolean phosh_rotation_manager_set_mode (PhoshRotationManager *self, + PhoshRotationManagerMode mode); +void phosh_rotation_manager_set_transform (PhoshRotationManager *self, + PhoshMonitorTransform transform); +PhoshMonitorTransform phosh_rotation_manager_get_transform (PhoshRotationManager *self); +PhoshMonitor *phosh_rotation_manager_get_monitor (PhoshRotationManager *self); +void phosh_rotation_manager_set_monitor (PhoshRotationManager *self, + PhoshMonitor *monitor); + +G_END_DECLS diff --git a/src/run-command-dialog.c b/src/run-command-dialog.c new file mode 100644 index 000000000..033fac893 --- /dev/null +++ b/src/run-command-dialog.c @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#define G_LOG_DOMAIN "phosh-run-command-dialog" + +#include "run-command-dialog.h" +#include "phosh-config.h" + +/** + * PhoshRunCommandDialog: + * + * A modal dialog to run commands from + * + * The #PhoshRunCommandDialog is used to run commands (e.g. via Alt+F2). + */ + +enum { + SUBMITTED, + CANCELLED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = {0}; + +typedef struct _PhoshRunCommandDialog { + PhoshSystemModalDialog parent; + GtkWidget *entry_command; + GtkLabel *lbl_description; +} PhoshRunCommandDialog; + +G_DEFINE_TYPE (PhoshRunCommandDialog, phosh_run_command_dialog, PHOSH_TYPE_SYSTEM_MODAL_DIALOG) + +static void +on_activated (PhoshRunCommandDialog *self, GtkEntry *entry) +{ + const char *command = gtk_entry_get_text (entry); + + g_signal_emit (self, signals[SUBMITTED], 0, command); +} + +static void +on_run_command_dialog_canceled (PhoshRunCommandDialog *self) +{ + g_return_if_fail (PHOSH_IS_RUN_COMMAND_DIALOG (self)); + g_signal_emit (self, signals[CANCELLED], 0); +} + +static void +phosh_run_command_dialog_finalize (GObject *obj) +{ + PhoshRunCommandDialog *self = PHOSH_RUN_COMMAND_DIALOG (obj); + + g_free (self->entry_command); + + G_OBJECT_CLASS (phosh_run_command_dialog_parent_class)->finalize (obj); +} + + +static void +on_text_changed (PhoshRunCommandDialog *self) +{ + g_return_if_fail (PHOSH_IS_RUN_COMMAND_DIALOG (self)); + + /* Make sure we reset any error message */ + phosh_run_command_dialog_set_message (self, NULL); +} + + +static void +phosh_run_command_dialog_class_init (PhoshRunCommandDialogClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_run_command_dialog_finalize; + + /** + * RunCommandDialog:submitted: + * + * The user submitted a command to run + */ + signals[SUBMITTED] = g_signal_new ("submitted", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 1, G_TYPE_STRING); + /** + * RunCommandDialog:cancelled: + * + * The user cancelled the dialog + */ + signals[CANCELLED] = g_signal_new ("cancelled", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/run-command-dialog.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshRunCommandDialog, entry_command); + gtk_widget_class_bind_template_child (widget_class, PhoshRunCommandDialog, lbl_description); + gtk_widget_class_bind_template_callback (widget_class, on_activated); + gtk_widget_class_bind_template_callback (widget_class, on_run_command_dialog_canceled); +} + +static void +phosh_run_command_dialog_init (PhoshRunCommandDialog *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + phosh_run_command_dialog_set_message (self, NULL); + g_signal_connect_swapped (self->entry_command, "notify::text", G_CALLBACK (on_text_changed), self); +} + +GtkWidget * +phosh_run_command_dialog_new (void) +{ + return g_object_new (PHOSH_TYPE_RUN_COMMAND_DIALOG, NULL); +} + +void +phosh_run_command_dialog_set_message (PhoshRunCommandDialog *self, + const char *message) +{ + if (message == NULL) + message = _("Press ESC to close"); + + gtk_label_set_label (self->lbl_description, message); +} diff --git a/src/run-command-dialog.h b/src/run-command-dialog.h new file mode 100644 index 000000000..512f1620a --- /dev/null +++ b/src/run-command-dialog.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#pragma once + +#include "system-modal-dialog.h" +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_RUN_COMMAND_DIALOG (phosh_run_command_dialog_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshRunCommandDialog, phosh_run_command_dialog, PHOSH, RUN_COMMAND_DIALOG, PhoshSystemModalDialog) + +GtkWidget *phosh_run_command_dialog_new (void); +void phosh_run_command_dialog_set_message (PhoshRunCommandDialog *self, + const char *message); + +G_END_DECLS diff --git a/src/run-command-manager.c b/src/run-command-manager.c new file mode 100644 index 000000000..97f2e656d --- /dev/null +++ b/src/run-command-manager.c @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#define G_LOG_DOMAIN "phosh-run-command-manager" + +#include "run-command-manager.h" +#include "run-command-dialog.h" +#include "shell-priv.h" +#include "util.h" + +#define KEYBINDINGS_SCHEMA_ID_DESKTOP_WM "org.gnome.desktop.wm.keybindings" +#define KEYBINDING_KEY_RUN_DIALOG "panel-run-dialog" + +/** + * PhoshRunCommandManager: + * + * Handles the run-command-dialog + * + * The interface is responsible to handle the non-ui parts of a + * #PhoshRunCommandDialog. + */ + +typedef struct _PhoshRunCommandManager { + GObject parent; + PhoshRunCommandDialog *dialog; + GStrv action_names; + GSettings *settings; +} PhoshRunCommandManager; + +G_DEFINE_TYPE (PhoshRunCommandManager, phosh_run_command_manager, G_TYPE_OBJECT) + +static void +cleanup_child_process (GPid pid, gint status, void *user_data) +{ + g_autoptr (GError) error = NULL; + + g_spawn_close_pid (pid); + + if (!g_spawn_check_wait_status (status, &error)) + g_warning ("Could not end child process: %s\n", error->message); +} + +static gboolean +run_command (char *command) +{ + GPid child_pid; + g_auto (GStrv) argv = NULL; + g_autoptr (GError) error = NULL; + + if (!g_shell_parse_argv (command, NULL, &argv, &error)) { + g_warning ("Could not parse command: %s\n", error->message); + return FALSE; + } + if (g_spawn_async (NULL, + argv, + NULL, + G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_SEARCH_PATH | + G_SPAWN_STDOUT_TO_DEV_NULL | + G_SPAWN_STDERR_TO_DEV_NULL, + NULL, + NULL, + &child_pid, + &error)) { + g_child_watch_add (child_pid, cleanup_child_process, NULL); + return TRUE; + } + + g_warning ("Could not run command: %s\n", error->message); + return FALSE; +} + +static void +on_run_command_dialog_submitted (PhoshRunCommandManager *self, char *command) +{ + g_autofree char *msg = NULL; + + g_return_if_fail (PHOSH_IS_RUN_COMMAND_DIALOG (self->dialog)); + g_return_if_fail (command); + + if (run_command (command)) { + g_clear_pointer ((PhoshSystemModalDialog**)&self->dialog, phosh_system_modal_dialog_close); + } else { + msg = g_strdup_printf (_("Running '%s' failed"), command); + phosh_run_command_dialog_set_message (self->dialog, msg); + } +} + +static void +on_run_command_dialog_cancelled (PhoshRunCommandManager *self) +{ + g_return_if_fail (PHOSH_IS_RUN_COMMAND_DIALOG (self->dialog)); + + g_clear_pointer ((PhoshSystemModalDialog**)&self->dialog, phosh_system_modal_dialog_close); +} + +static void +show_run_command_dialog (GSimpleAction *action, GVariant *param, gpointer data) +{ + GtkWidget *dialog; + PhoshRunCommandManager *self = PHOSH_RUN_COMMAND_MANAGER (data); + + if (self->dialog) + return; + dialog = phosh_run_command_dialog_new (); + self->dialog = PHOSH_RUN_COMMAND_DIALOG (dialog); + gtk_widget_set_visible (GTK_WIDGET (self->dialog), TRUE); + g_object_connect (self->dialog, + "swapped-object-signal::submitted", G_CALLBACK (on_run_command_dialog_submitted), self, + "swapped-object-signal::cancelled", G_CALLBACK (on_run_command_dialog_cancelled), self, + NULL); +} + +static void +add_keybindings (PhoshRunCommandManager *self) +{ + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + g_autoptr (GArray) actions = g_array_new (FALSE, TRUE, sizeof (GActionEntry)); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_RUN_DIALOG, + show_run_command_dialog); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry *)actions->data, + actions->len, + self); + self->action_names = g_strv_builder_end (builder); +} + +static void +on_keybindings_changed (PhoshRunCommandManager *self) +{ + g_debug ("Updating keybindings in run-command-manager"); + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + add_keybindings (self); +} + +static void +phosh_run_command_manager_constructed (GObject *object) +{ + PhoshRunCommandManager *self = PHOSH_RUN_COMMAND_MANAGER (object); + + G_OBJECT_CLASS (phosh_run_command_manager_parent_class)->constructed (object); + g_signal_connect_swapped (self->settings, + "changed::" KEYBINDING_KEY_RUN_DIALOG, + G_CALLBACK (on_keybindings_changed), + self); + add_keybindings (self); +} + +static void +phosh_run_command_manager_dispose (GObject *object) +{ + PhoshRunCommandManager *self = PHOSH_RUN_COMMAND_MANAGER (object); + + g_clear_pointer (&self->dialog, phosh_cp_widget_destroy); + g_clear_pointer (&self->action_names, g_strfreev); + g_clear_object (&self->settings); + G_OBJECT_CLASS (phosh_run_command_manager_parent_class)->dispose (object); +} + +static void +phosh_run_command_manager_class_init (PhoshRunCommandManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_run_command_manager_constructed; + object_class->dispose = phosh_run_command_manager_dispose; +} + +static void +phosh_run_command_manager_init (PhoshRunCommandManager *self) +{ + self->settings = g_settings_new (KEYBINDINGS_SCHEMA_ID_DESKTOP_WM); +} + +PhoshRunCommandManager * +phosh_run_command_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_RUN_COMMAND_MANAGER, NULL); +} diff --git a/src/run-command-manager.h b/src/run-command-manager.h new file mode 100644 index 000000000..3f0c18064 --- /dev/null +++ b/src/run-command-manager.h @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Florian Loers + */ + +#pragma once + +#include + +#define PHOSH_TYPE_RUN_COMMAND_MANAGER (phosh_run_command_manager_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshRunCommandManager, phosh_run_command_manager, PHOSH, RUN_COMMAND_MANAGER, GObject); + +PhoshRunCommandManager *phosh_run_command_manager_new (void); diff --git a/src/screen-saver-manager.c b/src/screen-saver-manager.c new file mode 100644 index 000000000..9f9439f17 --- /dev/null +++ b/src/screen-saver-manager.c @@ -0,0 +1,1023 @@ +/* + * Copyright (C) 2019-2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-screen-saver-manager" + +#include "screen-saver-manager.h" +#include "session-presence.h" +#include "shell-priv.h" +#include "idle-manager.h" +#include "login1-manager-dbus.h" +#include "login1-session-dbus.h" +#include "lockscreen-manager.h" +#include "session-presence.h" +#include "util.h" + +#include +#include + +#define LONG_PRESS_TIMEOUT 1 /* seconds */ + +/** + * PhoshScreenSaverManager: + * + * Provides the org.gnome.ScreenSaver DBus interface and handles logind's Session + * + * This handles the `org.gnome.ScreenSaver` DBus API for screen locking and unlocking. + * It also handles the login1 session parts since those are closely related and this + * keeps #PhoshLockscreenManager free of any session related DBus handling. + * + * These settings influence screen blanking and locking: + * + * `org.gnome.desktop.session idle-delay`: The session is considered idle after that many + * seconds of inactivity. This isn't monitored by phosh directly but is done by gnome-session that + * in turn uses `GnomeIdleMonitor` which then uses `/org/gnome/Mutter/IdleMonitor/Core` on DBus. + * `/org/gnome/Mutter/IdleMonitor/Core` is implemented by #PhoshIdleManager which in turn gets + * it from phoc via a Wayland protocol. + * + * `org.gnome.desktop.screensaver` `lock-enabled`: Whether the screen should be locked after + * the screen-saver is activated. + * + * `org.gnome.desktop.screensaver` `lock-delay`: How long after screen-saver activation should + * the screen be locked. + */ + +#define SCREEN_SAVER_DBUS_NAME "org.gnome.ScreenSaver" + +#define LOGIN_BUS_NAME "org.freedesktop.login1" +#define LOGIN_OBJECT_PATH "/org/freedesktop/login1" + +enum { + PROP_0, + PROP_LOCKSCREEN_MANAGER, + PROP_LOCK_ENABLED, + PROP_LOCK_DELAY, + PROP_ACTIVE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + PB_LONG_PRESS, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +static void phosh_screen_saver_manager_screen_saver_iface_init (PhoshDBusScreenSaverIface *iface); + +typedef struct _PhoshScreenSaverManager +{ + PhoshDBusScreenSaverSkeleton parent; + + int idle_id; + int dbus_name_id; + PhoshLockscreenManager *lockscreen_manager; + PhoshSessionPresence *presence; /* gnome-session's presence interface */ + gboolean active; + + /* Power button */ + guint long_press_id; + + GSettings *settings; + gboolean lock_enabled; + gboolean lock_delay; + guint lock_delay_timer_id; + int inhibit_pwr_btn_fd; + int inhibit_suspend_fd; + PhoshMonitor *primary_monitor; + + PhoshDBusLoginSession *logind_session_proxy; + PhoshDBusLoginManager *logind_manager_proxy; + + GCancellable *cancel; +} PhoshScreenSaverManager; + +G_DEFINE_TYPE_WITH_CODE (PhoshScreenSaverManager, + phosh_screen_saver_manager, + PHOSH_DBUS_TYPE_SCREEN_SAVER_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_SCREEN_SAVER, + phosh_screen_saver_manager_screen_saver_iface_init)); + + +static void +on_lock_delay_timer_expired (gpointer data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (data); + + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, TRUE); + + self->lock_delay_timer_id = 0; +} + + +static void +unarm_lock_delay_timer (PhoshScreenSaverManager *self, const char *reason) +{ + g_debug ("Unarming lock delay timer on %s", reason); + g_clear_handle_id (&self->lock_delay_timer_id, g_source_remove); +} + + +static void +arm_lock_delay_timer (PhoshScreenSaverManager *self, gboolean active, gboolean lock) +{ + if (!active || !lock) { + unarm_lock_delay_timer (self, "arm"); + return; + } + + /* Already locked, no locking to do */ + if (phosh_lockscreen_manager_get_locked (self->lockscreen_manager)) + return; + + /* no delay, lock right away */ + if (self->lock_delay == 0) { + unarm_lock_delay_timer (self, "arm"); + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, TRUE); + return; + } + + /* Timer already ticking, don't rearm */ + if (self->lock_delay_timer_id) + return; + + g_debug ("Arming lock delay timer for %d seconds", self->lock_delay); + self->lock_delay_timer_id = g_timeout_add_seconds_once (self->lock_delay, + on_lock_delay_timer_expired, + self); + g_source_set_name_by_id (self->lock_delay_timer_id, "[phosh] lock_delay_timer"); +} + + +/* Activate or deactivate screen blank based on `active`. If `lock` is %TRUE locking + is also enabled (honoring the configured delay) */ +static void +screen_saver_set_active (PhoshScreenSaverManager *self, gboolean active, gboolean lock) +{ + if (self->active == active) + return; + + g_debug ("Activating screen saver: %d, lock: %d, lock_delay: %d", active, lock, + self->lock_delay); + + /* on_primary_monitor_power_mode_changed will update self->active once the power mode is set */ + phosh_shell_enable_power_save (phosh_shell_get_default (), active); + + /* Don't wait for on_primary_monitor_power_mode_changed to activate the lock delay timer in + case that fails on the compositor side - we don't want to miss the screen lock */ + arm_lock_delay_timer (self, active, lock); +} + + +static gboolean +on_long_press (gpointer data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (data); + + g_debug ("Power button long press detected"); + + phosh_trigger_feedback ("button-pressed"); + g_signal_emit (self, signals[PB_LONG_PRESS], 0); + + self->long_press_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +on_power_button_pressed (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (data); + static gboolean state_on_press; + gboolean press = g_variant_get_boolean (param); + + /* We only detect long press when screen is active */ + if (press && self->active == FALSE) { + if (self->long_press_id) { + g_warning ("Long press timer already active"); + g_clear_handle_id (&self->long_press_id, g_source_remove); + } + self->long_press_id = g_timeout_add_seconds (LONG_PRESS_TIMEOUT, + on_long_press, + self); + g_source_set_name_by_id (self->long_press_id, "[PhoshScreensaverManager] long press"); + } + + /* Press already unblanks since presence status changes due to key press so nothing to do here */ + if (press) { + state_on_press = self->active; + return; + } + + /* If state changed during press, we're done (unblank) */ + if (self->active != state_on_press) + return; + + /* screen already blanked, no need to do anything on key release */ + if (self->active) + return; + + /* The long press triggered so don't change screen saver state */ + if (self->long_press_id == 0) + return; + + g_debug ("Power button released, activating screensaver"); + screen_saver_set_active (self, TRUE, self->lock_enabled); + + /* Disable long press timer */ + g_clear_handle_id (&self->long_press_id, g_source_remove); +} + + +static void +phosh_screen_saver_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (object); + + switch (property_id) { + case PROP_LOCKSCREEN_MANAGER: + self->lockscreen_manager = g_value_dup_object (value); + break; + case PROP_LOCK_ENABLED: + self->lock_enabled = g_value_get_boolean (value); + break; + case PROP_LOCK_DELAY: + self->lock_delay = g_value_get_int (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_screen_saver_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (object); + + switch (property_id) { + case PROP_LOCKSCREEN_MANAGER: + g_value_set_object (value, self->lockscreen_manager); + break; + case PROP_LOCK_ENABLED: + g_value_set_boolean (value, self->lock_enabled); + break; + case PROP_LOCK_DELAY: + g_value_set_int (value, self->lock_delay); + break; + case PROP_ACTIVE: + self->active = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static gboolean +handle_get_active (PhoshDBusScreenSaver *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self), FALSE); + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self->lockscreen_manager), FALSE); + + g_debug ("DBus call GetActive: %d", self->active); + + phosh_dbus_screen_saver_complete_get_active (skeleton, invocation, self->active); + + return TRUE; +} + + +static gboolean +handle_get_active_time (PhoshDBusScreenSaver *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (skeleton); + guint delta = 0; /* in seconds */ + guint64 active; + + g_return_val_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self), FALSE); + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self->lockscreen_manager), FALSE); + + active = phosh_lockscreen_manager_get_active_time (self->lockscreen_manager); + if (active) + delta = (g_get_monotonic_time () - active) / 1000000; + + g_debug ("DBus GetActiveTime: %u", delta); + phosh_dbus_screen_saver_complete_get_active_time (skeleton, invocation, delta); + + return TRUE; +} + +static gboolean +handle_lock (PhoshDBusScreenSaver *skeleton, + GDBusMethodInvocation *invocation) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self), FALSE); + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self->lockscreen_manager), FALSE); + + g_debug ("DBus call lock"); + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, TRUE); + /* TODO: wait a little before blanking */ + screen_saver_set_active (self, TRUE, TRUE); + + phosh_dbus_screen_saver_complete_lock (skeleton, invocation); + + return TRUE; +} + + +static gboolean +handle_set_active (PhoshDBusScreenSaver *skeleton, + GDBusMethodInvocation *invocation, + gboolean active) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (skeleton); + + g_return_val_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self), FALSE); + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self->lockscreen_manager), FALSE); + + g_debug ("DBus call SetActive: %d, lock-enabled: %d", active, self->lock_enabled); + screen_saver_set_active (self, active, self->lock_enabled); + + phosh_dbus_screen_saver_complete_set_active (skeleton, invocation); + + return TRUE; +} + + +static void +phosh_screen_saver_manager_screen_saver_iface_init (PhoshDBusScreenSaverIface *iface) +{ + iface->handle_get_active = handle_get_active; + iface->handle_get_active_time = handle_get_active_time; + iface->handle_lock = handle_lock; + iface->handle_set_active = handle_set_active; +} + +static void +notify_active_changed (PhoshScreenSaverManager *self) +{ + GDBusInterfaceSkeleton *skeleton; + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + skeleton = G_DBUS_INTERFACE_SKELETON (self); + + g_debug ("Signaling ActiveChanged: %d", self->active); + g_dbus_connection_emit_signal (g_dbus_interface_skeleton_get_connection (skeleton), + NULL, + g_dbus_interface_skeleton_get_object_path (skeleton), + SCREEN_SAVER_DBUS_NAME, + "ActiveChanged", + g_variant_new ("(b)", self->active), + NULL); + if (self->active) { + g_debug ("Uninhibited logind suspend handling"); + phosh_clear_fd (&self->inhibit_suspend_fd, NULL); + } +} + +static void +on_inhibit_suspend_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean success; + PhoshScreenSaverManager *self; + PhoshDBusLoginManager *proxy; + g_autoptr (GError) err = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + g_autoptr (GVariant) out_pipe_fd = NULL; + int idx; + + proxy = PHOSH_DBUS_LOGIN_MANAGER (source_object); + success = phosh_dbus_login_manager_call_inhibit_finish (proxy, + &out_pipe_fd, + &fd_list, + res, + &err); + if (!success) { + phosh_async_error_warn (err, "Failed to inhibit suspend"); + return; + } + + g_return_if_fail (fd_list && g_unix_fd_list_get_length (fd_list) == 1); + + self = PHOSH_SCREEN_SAVER_MANAGER (user_data); + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + g_variant_get (out_pipe_fd, "h", &idx); + self->inhibit_suspend_fd = g_unix_fd_list_get (fd_list, idx, &err); + if (self->inhibit_suspend_fd < 0) { + g_warning ("Failed to get suspend inhibit fd: %s", err->message); + return; + } + + g_debug ("Inhibited logind suspend handling"); +} + +static void +phosh_screen_saver_manager_inhibit_suspend (PhoshScreenSaverManager *self) +{ + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + phosh_dbus_login_manager_call_inhibit (self->logind_manager_proxy, + "sleep", + g_get_user_name (), + "Phosh handling suspend", + "delay", + NULL, + self->cancel, + on_inhibit_suspend_finished, + self); +} + +static void +phosh_screen_saver_manager_wakeup_screen (PhoshScreenSaverManager *self) +{ + GDBusInterfaceSkeleton *skeleton; + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + skeleton = G_DBUS_INTERFACE_SKELETON (self); + g_debug ("Signaling WakeUpScreen"); + g_dbus_connection_emit_signal (g_dbus_interface_skeleton_get_connection (skeleton), + NULL, + g_dbus_interface_skeleton_get_object_path (skeleton), + SCREEN_SAVER_DBUS_NAME, + "WakeUpScreen", + NULL, + NULL); +} + + +static void +on_wakeup_screen_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (data); + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + phosh_screen_saver_manager_wakeup_screen (self); +} + + +static void +on_lockscreen_manager_wakeup_outputs (PhoshScreenSaverManager *self, + PhoshLockscreenManager *lockscreen_manager) +{ + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (lockscreen_manager)); + + phosh_screen_saver_manager_wakeup_screen (self); +} + + +static void +on_locked_hint_set (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusLoginSession *proxy = PHOSH_DBUS_LOGIN_SESSION (source_object); + g_autoptr (GError) err = NULL; + gboolean success; + + success = phosh_dbus_login_session_call_set_locked_hint_finish (proxy, + res, + &err); + if (!success) + g_warning ("Failed to send locked hint: %s", err->message); +} + + +static void +on_lockscreen_manager_locked_changed (PhoshScreenSaverManager *self) +{ + gboolean locked; + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + locked = phosh_lockscreen_manager_get_locked (self->lockscreen_manager); + if (self->logind_session_proxy) { + phosh_dbus_login_session_call_set_locked_hint (self->logind_session_proxy, + locked, + self->cancel, + on_locked_hint_set, + NULL); + } + + if (locked == TRUE) + return; + + unarm_lock_delay_timer (self, "unlock"); +} + + +static void +on_logind_lock (PhoshScreenSaverManager *self, PhoshDBusLoginSession *proxy) +{ + g_debug ("Locking request via logind1"); + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, TRUE); +} + + +static void +on_logind_unlock (PhoshScreenSaverManager *self, PhoshDBusLoginSession *proxy) +{ + g_debug ("Unlocking request via logind1"); + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, FALSE); +} + + +static void +on_logind_prepare_for_sleep (PhoshScreenSaverManager *self, + gboolean suspending, + PhoshDBusLoginManager *proxy) +{ + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + g_debug ("Got PrepareForSleep signal: %s", suspending ? "suspend" : "resume"); + if (suspending) { + phosh_lockscreen_manager_set_locked (self->lockscreen_manager, TRUE); + } else { + PhoshIdleManager *idle_manager; + + phosh_screen_saver_manager_wakeup_screen (self); + + idle_manager = phosh_idle_manager_get_default (); + phosh_idle_manager_reset_timers (idle_manager); + phosh_screen_saver_manager_inhibit_suspend (self); + } +} + + +static void +on_presence_status_changed (PhoshScreenSaverManager *self, guint32 status, gpointer *data) +{ + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + g_debug ("Presence status changed: %d", status); + + if (status == PHOSH_SESSION_PRESENCE_STATUS_IDLE) + screen_saver_set_active (self, TRUE, self->lock_enabled); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (user_data); + + g_debug ("Acquired name %s", name); + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + /* Connect to lockscreen manager once we have a valid connection so we don't + end up sending out signals early */ + g_object_connect ( + self->lockscreen_manager, + "swapped-object-signal::wakeup-outputs", G_CALLBACK (on_lockscreen_manager_wakeup_outputs), self, + "swapped-object-signal::notify::locked", G_CALLBACK (on_lockscreen_manager_locked_changed), self, + NULL); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (user_data); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + "/org/gnome/ScreenSaver", + NULL); +} + + +static void +phosh_screen_saver_manager_dispose (GObject *object) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + unarm_lock_delay_timer (self, "dispose"); + g_clear_handle_id (&self->idle_id, g_source_remove); + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + phosh_clear_fd (&self->inhibit_pwr_btn_fd, NULL); + phosh_clear_fd (&self->inhibit_suspend_fd, NULL); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_object (&self->settings); + g_clear_object (&self->lockscreen_manager); + g_clear_object (&self->logind_session_proxy); + g_clear_object (&self->logind_manager_proxy); + g_clear_object (&self->primary_monitor); + + g_clear_object (&self->presence); + g_clear_handle_id (&self->long_press_id, g_source_remove); + + G_OBJECT_CLASS (phosh_screen_saver_manager_parent_class)->dispose (object); +} + + +static void +on_logind_get_session_proxy_finish (GObject *object, + GAsyncResult *res, + PhoshScreenSaverManager *self) +{ + g_autoptr (GError) err = NULL; + + self->logind_session_proxy = phosh_dbus_login_session_proxy_new_for_bus_finish ( + res, &err); + if (!self->logind_session_proxy) { + phosh_dbus_service_error_warn (err, "Failed to get login1 session proxy"); + return; + } + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + /* finally register signals */ + g_object_connect ( + self->logind_session_proxy, + "swapped-object-signal::lock", G_CALLBACK (on_logind_lock), self, + "swapped-object-signal::unlock", G_CALLBACK (on_logind_unlock), self, + NULL); +} + + +static void +on_logind_manager_get_session_finished (PhoshDBusLoginManager *object, + GAsyncResult *res, + PhoshScreenSaverManager *self) +{ + g_autofree char *object_path = NULL; + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_login_manager_call_get_session_finish ( + object, &object_path, res, &err)) { + phosh_async_error_warn (err, "Failed to get session"); + return; + } + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + /* Register a proxy for this session */ + phosh_dbus_login_session_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + LOGIN_BUS_NAME, + object_path, + self->cancel, + (GAsyncReadyCallback)on_logind_get_session_proxy_finish, + self); +} + + +static void +on_inhibit_pwr_button_finished (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + PhoshScreenSaverManager *self; + gboolean success; + PhoshDBusLoginManager *proxy; + g_autoptr (GError) err = NULL; + g_autoptr (GUnixFDList) fd_list = NULL; + g_autoptr (GVariant) out_pipe_fd = NULL; + int idx; + + proxy = PHOSH_DBUS_LOGIN_MANAGER (source_object); + success = phosh_dbus_login_manager_call_inhibit_finish (proxy, + &out_pipe_fd, + &fd_list, + res, + &err); + if (!success) { + phosh_async_error_warn (err, "Failed to inhibit power button"); + return; + } + + g_return_if_fail (fd_list && g_unix_fd_list_get_length (fd_list) == 1); + + self = PHOSH_SCREEN_SAVER_MANAGER (user_data); + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + g_variant_get (out_pipe_fd, "h", &idx); + self->inhibit_pwr_btn_fd = g_unix_fd_list_get (fd_list, idx, &err); + if (self->inhibit_pwr_btn_fd < 0) { + g_warning ("Failed to get power button inhibit fd: %s", err->message); + return; + } + + g_debug ("Inhibited logind power button handling"); +} + + +static void +on_logind_manager_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + PhoshScreenSaverManager *self) +{ + g_autoptr (GError) err = NULL; + g_autofree char *session_id = NULL; + + self->logind_manager_proxy = + phosh_dbus_login_manager_proxy_new_for_bus_finish (res, &err); + + if (!self->logind_manager_proxy) { + phosh_dbus_service_error_warn (err, "Failed to get login1 manager proxy"); + return; + } + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + g_signal_connect_swapped (self->logind_manager_proxy, + "prepare-for-sleep", + G_CALLBACK (on_logind_prepare_for_sleep), + self); + + /* If we find a session get it object path */ + if (phosh_find_systemd_session (&session_id)) { + g_debug ("Logind session %s", session_id); + + phosh_dbus_login_manager_call_get_session ( + self->logind_manager_proxy, + session_id, + self->cancel, + (GAsyncReadyCallback)on_logind_manager_get_session_finished, + self); + } else { + g_debug ("No Login session, screen blank/lock will be unreliable"); + } + + g_debug ("Connected to logind's session interface"); + + phosh_dbus_login_manager_call_inhibit (self->logind_manager_proxy, + "handle-power-key", + g_get_user_name (), + "Phosh handling power key", + "block", + NULL, + self->cancel, + on_inhibit_pwr_button_finished, + self); + + phosh_screen_saver_manager_inhibit_suspend (self); +} + + +static void +on_primary_monitor_power_mode_changed (PhoshScreenSaverManager *self, + GParamSpec *pspec, + PhoshMonitor *monitor) +{ + gboolean active; + PhoshMonitorPowerSaveMode mode; + + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + mode = phosh_monitor_get_power_save_mode (monitor); + + active = (mode == PHOSH_MONITOR_POWER_SAVE_MODE_OFF); + g_debug ("Screensaver marked as %sactive", active ? "" : "in"); + if (active != self->active) { + self->active = active; + g_object_notify_by_pspec(G_OBJECT (self), props[PROP_ACTIVE]); + notify_active_changed (self); + } + + if (active) { + arm_lock_delay_timer (self, active, self->lock_enabled); + } else { + unarm_lock_delay_timer (self, "power mode change"); + } +} + + +static void +on_primary_monitor_changed (PhoshScreenSaverManager *self, + GParamSpec *psepc, + PhoshShell *shell) +{ + g_return_if_fail (PHOSH_IS_SHELL (shell)); + g_return_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self)); + + if (self->primary_monitor) + g_signal_handlers_disconnect_by_data(self->primary_monitor, self); + + g_set_object (&self->primary_monitor, phosh_shell_get_primary_monitor (shell)); + + if (self->primary_monitor == NULL) { + unarm_lock_delay_timer (self, "primary monitor change"); + return; + } + + g_signal_connect_object (self->primary_monitor, + "notify::power-mode", + G_CALLBACK (on_primary_monitor_power_mode_changed), + self, + G_CONNECT_SWAPPED); + on_primary_monitor_power_mode_changed (self, NULL, self->primary_monitor); +} + + +static gboolean +on_idle (PhoshScreenSaverManager *self) +{ + /* Connect to logind's session manager */ + phosh_dbus_login_manager_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + LOGIN_BUS_NAME, + LOGIN_OBJECT_PATH, + self->cancel, + (GAsyncReadyCallback) on_logind_manager_proxy_new_for_bus_finish, + self); + + g_signal_connect_swapped (phosh_shell_get_default (), + "notify::primary-monitor", + G_CALLBACK (on_primary_monitor_changed), + self); + on_primary_monitor_changed (self, NULL, phosh_shell_get_default ()); + + self->idle_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +add_keybindings (PhoshScreenSaverManager *self) +{ + GActionEntry entries[] = { + { "XF86PowerOff", on_power_button_pressed, "b" }, + }; + + /* g-s-manager's grab_single_accelerator makes sure g-s-d doesn't bind it */ + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry*)entries, + G_N_ELEMENTS (entries), + self); +} + + +static void +phosh_screen_saver_manager_constructed (GObject *object) +{ + PhoshScreenSaverManager *self = PHOSH_SCREEN_SAVER_MANAGER (object); + + G_OBJECT_CLASS (phosh_screen_saver_manager_parent_class)->constructed (object); + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + SCREEN_SAVER_DBUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); + + g_return_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (self->lockscreen_manager)); + + self->settings = g_settings_new ("org.gnome.desktop.screensaver"); + g_settings_bind (self->settings, "lock-enabled", self, "lock-enabled", G_SETTINGS_BIND_GET); + g_settings_bind (self->settings, "lock-delay", self, "lock-delay", G_SETTINGS_BIND_GET); + + add_keybindings (self); + + self->presence = phosh_session_presence_get_default_failable (); + if (self->presence) { + g_signal_connect_swapped (self->presence, + "status-changed", + (GCallback) on_presence_status_changed, + self); + } + + /* Perform login1 setup when idle */ + self->idle_id = g_idle_add ((GSourceFunc)on_idle, self); + g_source_set_name_by_id (self->idle_id, "[PhoshScreenSaverManager] idle"); +} + + +static void +phosh_screen_saver_manager_class_init (PhoshScreenSaverManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_screen_saver_manager_constructed; + object_class->dispose = phosh_screen_saver_manager_dispose; + object_class->set_property = phosh_screen_saver_manager_set_property; + object_class->get_property = phosh_screen_saver_manager_get_property; + + props[PROP_LOCKSCREEN_MANAGER] = + g_param_spec_object ("lockscreen-manager", + "LockscreenManager", + "The lockscreen manager", + PHOSH_TYPE_LOCKSCREEN_MANAGER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + + /** + * PhoshScreenSaverManager:lock-enabled: + * + * Whether activating the screens saver should also lock the screen + */ + props[PROP_LOCK_ENABLED] = + g_param_spec_boolean ("lock-enabled", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshScreenSaverManager:lock-delay: + * + * Delay in seconds for screen lock after blank + */ + props[PROP_LOCK_DELAY] = + g_param_spec_int ("lock-delay", "", "", + 0, + G_MAXINT, + 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + /** + * PhoshScreenSaverManager:active + * + * Whether the screen saver is active + */ + props[PROP_ACTIVE] = + g_param_spec_boolean ("active", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshScreenSaverManager:pb-long-press + * + * Power button long press detected + */ + signals[PB_LONG_PRESS] = + g_signal_new ("pb-long-press", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_screen_saver_manager_init (PhoshScreenSaverManager *self) +{ + const GActionEntry entries[] = { + { .name = "screensaver.wakeup-screen", .activate = on_wakeup_screen_activated }, + }; + + self->cancel = g_cancellable_new (); + self->inhibit_suspend_fd = -1; + self->inhibit_pwr_btn_fd = -1; + + g_action_map_add_action_entries (G_ACTION_MAP (phosh_shell_get_default ()), + entries, + G_N_ELEMENTS (entries), + self); +} + + +PhoshScreenSaverManager * +phosh_screen_saver_manager_new (PhoshLockscreenManager *lockscreen_manager) +{ + return g_object_new (PHOSH_TYPE_SCREEN_SAVER_MANAGER, + "lockscreen-manager", lockscreen_manager, + NULL); +} diff --git a/src/screen-saver-manager.h b/src/screen-saver-manager.h new file mode 100644 index 000000000..e20e91cc0 --- /dev/null +++ b/src/screen-saver-manager.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "lockscreen-manager.h" + +#include "dbus/phosh-screen-saver-dbus.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SCREEN_SAVER_MANAGER (phosh_screen_saver_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshScreenSaverManager, phosh_screen_saver_manager, PHOSH, SCREEN_SAVER_MANAGER, + PhoshDBusScreenSaverSkeleton) + +PhoshScreenSaverManager *phosh_screen_saver_manager_new (PhoshLockscreenManager *lockscreen_manager); + +G_END_DECLS diff --git a/src/screenshot-manager.c b/src/screenshot-manager.c new file mode 100644 index 000000000..f5b72f007 --- /dev/null +++ b/src/screenshot-manager.c @@ -0,0 +1,1382 @@ +/* + * Copyright (C) 2021-2022 Purism SPC + * 2023-2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-screenshot-manager" + +#include "phosh-config.h" +#include "fader.h" +#include "phosh-wayland.h" +#include "notifications/notify-manager.h" +#include "screenshot-manager.h" +#include "shell-priv.h" +#include "util.h" +#include "wl-buffer.h" + +#include "dbus/phosh-screenshot-dbus.h" + +#include + +#include + +#define BUS_NAME "org.gnome.Shell.Screenshot" +#define OBJECT_PATH "/org/gnome/Shell/Screenshot" + +#define KEYBINDINGS_SCHEMA_ID "org.gnome.shell.keybindings" +#define KEYBINDING_KEY_SCREENSHOT "screenshot" + +#define FLASH_FADER_TIMEOUT 500 + +/** + * PhoshScreenshotManager: + * + * Screenshot interaction + * + * The #PhoshScreenshotManager is responsible for + * taking screenshots. + */ + +static void phosh_screenshot_manager_screenshot_iface_init ( + PhoshDBusScreenshotIface *iface); + +typedef enum { + FRAME_STATE_FAILURE = -1, + FRAME_STATE_UNKNOWN = 0, + FRAME_STATE_SUCCESS = 1, +} ScreencopyFrameState; + +typedef struct _ScreencopyFrame { + struct zwlr_screencopy_frame_v1 *frame; + uint32_t flags; + PhoshWlBuffer *buffer; + GdkPixbuf *pixbuf; + PhoshMonitor *monitor; + ScreencopyFrameState state; + PhoshScreenshotManager *manager; +} ScreencopyFrame; + +typedef struct { + GList *frames; + GDBusMethodInvocation *invocation; + gboolean flash; + char *filename; + guint num_outputs; + float max_scale; + GdkRectangle *area; + gboolean copy_to_clipboard; +} ScreencopyFrames; + +typedef struct { + guint child_watch_id; + GPid pid; + GDBusMethodInvocation *invocation; + GInputStream *stdout_; + GCancellable *cancel; + char read_buf[64]; + GString *response; +} SlurpArea; + + +typedef struct _PhoshScreenshotManager { + PhoshDBusScreenshotSkeleton parent; + + int dbus_name_id; + struct zwlr_screencopy_manager_v1 *wl_scm; + ScreencopyFrames *frames; + SlurpArea *slurp; + + PhoshFader *fader; + guint fader_id; + PhoshFader *opaque; + guint opaque_id; + + GdkPixbuf *for_clipboard; + + GStrv action_names; + GSettings *settings; + + GCancellable *cancel; +} PhoshScreenshotManager; + + +G_DEFINE_TYPE_WITH_CODE (PhoshScreenshotManager, + phosh_screenshot_manager, + PHOSH_DBUS_TYPE_SCREENSHOT_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_SCREENSHOT, + phosh_screenshot_manager_screenshot_iface_init)); + + +static void +slurp_area_dispose (SlurpArea *slurp) +{ + if (slurp->response) { + g_string_free (slurp->response, TRUE); + slurp->response = NULL; + } + g_cancellable_cancel (slurp->cancel); + g_clear_object (&slurp->cancel); + g_clear_object (&slurp->stdout_); +} + + +static void +screencopy_frame_dispose (ScreencopyFrame *frame) +{ + g_clear_pointer (&frame->buffer, phosh_wl_buffer_destroy); + g_clear_pointer (&frame->frame, zwlr_screencopy_frame_v1_destroy); + g_clear_object (&frame->pixbuf); + + if (frame->monitor) { + g_object_remove_weak_pointer (G_OBJECT (frame->monitor), (gpointer)&frame->monitor); + frame->monitor = NULL; + } + + g_free (frame); +} + + +static void +screencopy_frames_dispose (ScreencopyFrames *frames) +{ + g_clear_pointer (&frames->area, g_free); + g_clear_list (&frames->frames, (GDestroyNotify) screencopy_frame_dispose); + g_free (frames->filename); + g_free (frames); +} + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (ScreencopyFrames, screencopy_frames_dispose); + + +static void +screencopy_frame_handle_buffer (void *data, + struct zwlr_screencopy_frame_v1 *frame, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride) +{ + ScreencopyFrame *screencopy_frame = data; + + g_debug ("Handling buffer %dx%d for %s", width, height, screencopy_frame->monitor->name); + screencopy_frame->buffer = phosh_wl_buffer_new (format, width, height, stride); + g_return_if_fail (screencopy_frame->buffer); + + zwlr_screencopy_frame_v1_copy (frame, screencopy_frame->buffer->wl_buffer); +} + + +static void +screencopy_frame_handle_flags (void *data, + struct zwlr_screencopy_frame_v1 *frame, + uint32_t flags) +{ + ScreencopyFrame *screencopy_frame = data; + + g_return_if_fail (screencopy_frame); + + screencopy_frame->flags = flags; +} + +static gboolean +on_fader_timeout (gpointer user_data) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (user_data); + + g_clear_pointer (&self->fader, phosh_cp_widget_destroy); + + self->fader_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +show_fader (PhoshScreenshotManager *self) +{ + PhoshMonitor *monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + + self->fader_id = g_timeout_add (FLASH_FADER_TIMEOUT, on_fader_timeout, self); + g_source_set_name_by_id (self->fader_id, "[phosh] screenshot fader"); + self->fader = g_object_new (PHOSH_TYPE_FADER, + "monitor", monitor, + "style-class", "phosh-fader-flash-fade", + NULL); + gtk_widget_set_visible (GTK_WIDGET (self->fader), TRUE); +} + + +static void +screenshot_done (PhoshScreenshotManager *self, gboolean success) +{ + /* Invocation via DBus API */ + if (self->frames->invocation) { + phosh_dbus_screenshot_complete_screenshot (PHOSH_DBUS_SCREENSHOT (self), + self->frames->invocation, + success, + self->frames->filename ?: ""); + g_clear_pointer (&self->frames, screencopy_frames_dispose); + return; + } + + /* Internal screenshot */ + if (self->frames->filename) { + PhoshNotifyManager *nm = phosh_notify_manager_get_default (); + g_autoptr (GIcon) icon = g_themed_icon_new ("screenshot-portrait-symbolic"); + g_autoptr (PhoshNotification) noti = NULL; + g_autofree char *msg = NULL; + + if (success) { + g_autofree char *filename = NULL; + + filename = g_path_get_basename (self->frames->filename); + /* Translators: '%s' is the filename of a screenshot */ + msg = g_strdup_printf (_("Screenshot saved to '%s'"), filename); + } else { + msg = g_strdup (_("Failed to save screenshot")); + } + + noti = g_object_new (PHOSH_TYPE_NOTIFICATION, + "summary", _("Screenshot"), + "body", msg, + "image", icon, + NULL); + + phosh_notify_manager_add_shell_notification (nm, noti, 0, 5000); + g_clear_pointer (&self->frames->filename, g_free); + } + + if (!self->frames->filename && !self->frames->copy_to_clipboard) + g_clear_pointer (&self->frames, screencopy_frames_dispose); +} + + +static void +update_recent_files (PhoshScreenshotManager *self) +{ + g_autofree char *recent = g_build_filename (g_get_user_data_dir (), "recently-used.xbel", NULL); + g_autofree char *uri = NULL; + g_autoptr (GBookmarkFile) bookmarks = g_bookmark_file_new (); + g_autoptr (GError) err = NULL; + + g_return_if_fail (self->frames->filename); + + if (!g_bookmark_file_load_from_file (bookmarks, recent, &err)) { + if (!g_error_matches (err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) { + g_warning ("Failed to open bookarks %s: %s", recent, err->message); + return; + } + } + + uri = g_filename_to_uri (self->frames->filename, NULL, &err); + if (!uri) { + g_warning ("Failed to create bookmark uri for '%s': %s", uri, err->message); + return; + } + g_bookmark_file_add_application (bookmarks, uri, "Phosh", "gio open %u"); + + if (!g_bookmark_file_to_file (bookmarks, recent, &err)) { + g_warning ("Failed to save bookmarks %s: %s", recent, err->message); + return; + } +} + + +static char * +phosh_screenshot_manager_build_thumbnail_path (const char *uri) +{ + g_autoptr (GChecksum) checksum = g_checksum_new (G_CHECKSUM_MD5); + guint8 digest[16]; + gsize digest_len = sizeof (digest); + g_autofree char *name = NULL, *path = NULL; + + g_checksum_update (checksum, (const guchar *) uri, strlen (uri)); + g_checksum_get_digest (checksum, digest, &digest_len); + g_assert (digest_len == 16); + + name = g_strconcat (g_checksum_get_string (checksum), ".png", NULL); + path = g_build_filename (g_get_user_cache_dir (), + "thumbnails", + "normal", + name, + NULL); + + return g_steal_pointer (&path); +} + + +static void +on_save_thumbnail_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean success; + g_autoptr (GError) err = NULL; + + success = gdk_pixbuf_save_to_stream_finish (res, &err); + if (!success) + g_warning ("Failed to save thumbnail: %s", err->message); +} + + +#define THUMBNAIL_SIZE 128 + +static void +phosh_screenshot_manager_save_thumbnail (PhoshScreenshotManager *self, + const char *filename, + GdkPixbuf *pixbuf) +{ + int width, height; + double scale; + g_autoptr (GdkPixbuf) scaled = NULL; + g_autoptr (GFile) file = NULL; + g_autoptr (GFileOutputStream) stream = NULL; + g_autoptr (GError) err = NULL; + g_autofree char *thumbnail_name = NULL, *dirname = NULL, *uri = NULL; + g_autofree char *mtime_str = NULL, *width_str = NULL, *height_str = NULL; + g_autoptr (GDateTime) now = NULL; + + uri = g_filename_to_uri (filename, NULL, &err); + if (!uri) { + g_warning ("Failed to create thumbnail name for '%s': %s", filename, err->message); + return; + } + + thumbnail_name = phosh_screenshot_manager_build_thumbnail_path (uri); + dirname = g_path_get_dirname (thumbnail_name); + if (g_mkdir_with_parents (dirname, 0700) != 0) { + g_warning ("Failed to create thumbnail folder '%s'", dirname); + return; + } + + file = g_file_new_for_path (thumbnail_name); + stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, &err); + if (!stream) { + g_warning ("Failed to create thumbnail file %s: %s", thumbnail_name, err->message); + return; + } + + width = gdk_pixbuf_get_width (pixbuf); + height = gdk_pixbuf_get_height (pixbuf); + + scale = (double)THUMBNAIL_SIZE / MAX (width, height); + scaled = gdk_pixbuf_scale_simple (pixbuf, + floor (width * scale + 0.5), + floor (height * scale + 0.5), + GDK_INTERP_BILINEAR); + gdk_pixbuf_copy_options (pixbuf, scaled); + + now = g_date_time_new_now_local(); + mtime_str = g_strdup_printf ("%" G_GINT64_FORMAT, (gint64) g_date_time_to_unix (now)); + width_str = g_strdup_printf ("%d", width); + height_str = g_strdup_printf ("%d", height); + gdk_pixbuf_save_to_stream_async (scaled, + G_OUTPUT_STREAM (stream), + "png", + NULL, + on_save_thumbnail_ready, + NULL, + "tEXt::Thumb::Image::Width", width_str, + "tEXt::Thumb::Image::Height", height_str, + "tEXt::Thumb::URI", uri, + "tEXt::Thumb::MTime", mtime_str, + "tEXt::Software", "Phosh::Shell", + NULL); +} + + +static void +on_opaque_timeout (gpointer data) +{ + PhoshScreenshotManager *self = data; + GdkDisplay *display = gdk_display_get_default (); + GtkClipboard *clipboard; + + if (!display) { + g_critical ("Couldn't get GDK display"); + goto out; + } + + clipboard = gtk_clipboard_get_for_display (display, GDK_SELECTION_CLIPBOARD); + gtk_clipboard_set_image (clipboard, self->for_clipboard); + g_debug ("Updated clipboard"); + self->frames->copy_to_clipboard = FALSE; + screenshot_done (self, TRUE); + + out: + g_clear_object (&self->for_clipboard); + g_clear_pointer (&self->opaque, phosh_cp_widget_destroy); + self->opaque_id = 0; +} + + +static void +copy_to_clipboard (PhoshScreenshotManager *self, GdkPixbuf *pixbuf) +{ + PhoshMonitor *monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + + /* The Wayland clipboard only works if we have focus so use a fully opaque surface */ + self->opaque = g_object_new (PHOSH_TYPE_FADER, + "monitor", monitor, + "style-class", "phosh-fader-screenshot-opaque", + "kbd-interactivity", TRUE, + NULL); + self->for_clipboard = g_object_ref (pixbuf); + /* FIXME: Would be better to trigger when the opaque window is up and got + input focus but all such attempts failed */ + self->opaque_id = g_timeout_add_seconds_once (1, on_opaque_timeout, self); + g_source_set_name_by_id (self->opaque_id, "[phosh] screenshot opaque"); + + gtk_widget_set_visible (GTK_WIDGET (self->opaque), TRUE); +} + + +static void +on_save_pixbuf_ready (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + gboolean success; + g_autoptr (GError) err = NULL; + g_autoptr (PhoshScreenshotManager) self = PHOSH_SCREENSHOT_MANAGER (user_data); + + g_return_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (self)); + g_return_if_fail (self->frames->filename); + + success = gdk_pixbuf_save_to_stream_finish (res, &err); + if (!success) { + g_warning ("Failed to save screenshot: %s", err->message); + screenshot_done (self, FALSE); + return; + } + + if (!self->frames->invocation) + update_recent_files (self); + + phosh_screenshot_manager_save_thumbnail (self, + self->frames->filename, + GDK_PIXBUF (source_object)); + + if (self->frames->copy_to_clipboard) + copy_to_clipboard (self, GDK_PIXBUF (source_object)); + else + screenshot_done (self, success); +} + + +/* Taken from grim */ +static GdkRectangle +get_output_layout (PhoshScreenshotManager *self) +{ + GdkRectangle box; + guint x1 = G_MAXUINT, y1 = G_MAXUINT, x2 = 0, y2 = 0; + + for (GList *l = self->frames->frames; l; l = l->next) { + ScreencopyFrame *frame = l->data; + PhoshMonitor *monitor = PHOSH_MONITOR (frame->monitor); + + if (monitor->logical.x < x1) + x1 = monitor->logical.x; + + if (monitor->logical.y < y1) + y1 = monitor->logical.y; + + if (monitor->logical.x + monitor->logical.width > x2) + x2 = monitor->logical.x + monitor->logical.width; + + if (monitor->logical.y + monitor->logical.height > y2) + y2 = monitor->logical.y + monitor->logical.height; + } + + box.x = x1; + box.y = y1; + box.width = x2 - x1; + box.height = y2 - y1; + + return box; +} + + +static guint +get_angle (PhoshMonitorTransform transform) +{ + switch (transform) { + case PHOSH_MONITOR_TRANSFORM_FLIPPED: + case PHOSH_MONITOR_TRANSFORM_NORMAL: + return 0; + case PHOSH_MONITOR_TRANSFORM_FLIPPED_90: + case PHOSH_MONITOR_TRANSFORM_90: + return 270; + case PHOSH_MONITOR_TRANSFORM_FLIPPED_180: + case PHOSH_MONITOR_TRANSFORM_180: + return 180; + case PHOSH_MONITOR_TRANSFORM_FLIPPED_270: + case PHOSH_MONITOR_TRANSFORM_270: + return 90; + default: + g_return_val_if_reached (0); + } +} + +/** + * create_internal_file: + * @self: The screenshot manager + * @err: An error location + * + * Create a file for saving the screenshot. This is used when the the + * shell takes the screenshot e.g. via keybinding. See + * `build_dbus_filename` for the DBus case. + * + * Returns: An output stream that writes to the created file + */ +static GFileOutputStream * +create_internal_file (PhoshScreenshotManager *self, GError **err) + +{ + g_autoptr (GFile) dir = NULL; + g_autoptr (GDateTime) dt = NULL; + g_autofree char *dirname = NULL; + g_autofree char *timestamp = NULL; + const char *base_dir; + + g_assert (PHOSH_IS_SCREENSHOT_MANAGER (self)); + g_assert (err && *err == NULL); + + base_dir = g_get_user_special_dir (G_USER_DIRECTORY_PICTURES); + if (!base_dir) + base_dir = g_get_home_dir (); + + /* Translators: name of the folder beneath ~/Pictures used to store screenshots */ + dirname = g_build_filename (base_dir, _("Screenshots"), NULL); + dir = g_file_new_for_path (dirname); + if (!g_file_make_directory_with_parents (dir, NULL, err)) { + if (!g_error_matches (*err, G_IO_ERROR, G_IO_ERROR_EXISTS)) + return NULL; + } + + dt = g_date_time_new_now_local (); + timestamp = g_date_time_format (dt, "%Y-%m-%d %H:%M:%S"); + + for (int i = 0; i < 100; i++) { + g_autofree char *suffix = i ? g_strdup_printf ("-%d", i) : g_strdup (""); + g_autofree char *filename = NULL; + g_autofree char *path = NULL; + g_autoptr (GFile) file = NULL; + g_autoptr (GFileOutputStream) stream = NULL; + + /* Translators: Name of a screenshot file. The first '%s' is a timestamp + * like "2017-05-21 12-24-03" the 2nd '%s' is a possible suffix in case + * the file already exists like '-3' */ + filename = g_strdup_printf (_("Screenshot from %s%s.png"), timestamp, suffix); + path = g_build_filename (dirname, filename, NULL); + file = g_file_new_for_path (path); + stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, err); + if (stream) { + g_debug ("Saving screenshot to '%s'", path); + self->frames->filename = g_strdup (path); + return g_steal_pointer (&stream); + } + } + + g_warning ("Failed to build screenshot filename in '%s'", dirname); + return NULL; +} + +/* Got all pixbufs, prepare result */ +static void +submit_screenshot (PhoshScreenshotManager *self) +{ + g_autoptr (GError) err = NULL; + g_autoptr (GFileOutputStream) stream = NULL; + g_autoptr (GFile) file = NULL; + g_autoptr (GdkPixbuf) pixbuf = NULL; + GdkRectangle box; + float screenshot_scale = self->frames->max_scale; + + box = get_output_layout (self); + g_debug ("Screenshot of %d,%d %dx%d", box.x, box.y, box.width, box.height); + + pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, + TRUE, + 8, + box.width * screenshot_scale, + box.height * screenshot_scale); + + /* TODO: Using cairo would avoid lots of copies */ + for (GList *l = self->frames->frames; l; l = l->next) { + ScreencopyFrame *frame = l->data; + float scale; + /* how much this monitor gets enlarged based on its scale, >= 1.0 */ + double zoom; + g_autoptr (GdkPixbuf) transformed = NULL; + + if (frame->monitor == NULL) + continue; + + scale = phosh_monitor_get_fractional_scale (frame->monitor); + zoom = screenshot_scale / scale; + g_debug ("Screenshot of '%s' of %d,%d %dx%d, scale: %f", + frame->monitor->name, + frame->monitor->logical.x - box.x, + frame->monitor->logical.y - box.y, + frame->monitor->logical.width, + frame->monitor->logical.height, + scale); + + /* TODO: handle flips */ + transformed = gdk_pixbuf_rotate_simple (frame->pixbuf, + get_angle (frame->monitor->transform)); + gdk_pixbuf_composite (transformed, + pixbuf, + (frame->monitor->logical.x - box.x) * screenshot_scale, + (frame->monitor->logical.y - box.y) * screenshot_scale, + frame->monitor->logical.width * screenshot_scale, + frame->monitor->logical.height * screenshot_scale, + (frame->monitor->logical.x - box.x) * screenshot_scale, + (frame->monitor->logical.y - box.y) * screenshot_scale, + zoom, zoom, + GDK_INTERP_BILINEAR, + 255); + } + + if (self->frames->area) { + g_autoptr (GdkPixbuf) tmp = pixbuf; + + pixbuf = gdk_pixbuf_new (GDK_COLORSPACE_RGB, + TRUE, + 8, + self->frames->area->width * screenshot_scale, + self->frames->area->height * screenshot_scale); + gdk_pixbuf_copy_area (tmp, + (self->frames->area->x - box.x) * screenshot_scale, + (self->frames->area->y - box.y) * screenshot_scale, + self->frames->area->width * screenshot_scale, + self->frames->area->height * screenshot_scale, + pixbuf, + 0, + 0); + } + + if (self->frames->filename) { + file = g_file_new_for_path (self->frames->filename); + stream = g_file_create (file, G_FILE_CREATE_NONE, NULL, &err); + if (!stream) { + g_warning ("Failed to create screenshot %s: %s", self->frames->filename, err->message); + screenshot_done (self, FALSE); + return; + } + } else if (!self->frames->invocation) { + /* Generate filename for internal screenshot */ + stream = create_internal_file (self, &err); + if (!stream) { + g_warning ("Failed to create screenshot: %s", err->message); + screenshot_done (self, FALSE); + return; + } + } + + if (stream) { + /* on_save_to_pixbuf will trigger copy_to_clipboard if needed */ + gdk_pixbuf_save_to_stream_async (pixbuf, + G_OUTPUT_STREAM (stream), + "png", + self->cancel, + on_save_pixbuf_ready, + g_object_ref (self), + NULL); + } else if (self->frames->copy_to_clipboard) { + /* Copy to clipboard only */ + copy_to_clipboard (self, pixbuf); + } + + if (self->frames->flash) { + phosh_trigger_feedback ("screen-capture"); + show_fader (self); + } +} + + +static void +maybe_screencopy_done (PhoshScreenshotManager *self) +{ + guint failed = 0, done = 0; + + for (GList *l = self->frames->frames; l; l = l->next) { + ScreencopyFrame *frame = l->data; + + switch (frame->state) { + case FRAME_STATE_UNKNOWN: + return; + case FRAME_STATE_FAILURE: + failed++; + break; + case FRAME_STATE_SUCCESS: + done++; + break; + default: + g_warn_if_reached (); + break; + } + } + + /* Wait for all screencopies until we start merging all outputs */ + if (done + failed != self->frames->num_outputs) + return; + + /* With a failure no need to merge pixbufs */ + if (failed) { + phosh_dbus_screenshot_complete_screenshot (PHOSH_DBUS_SCREENSHOT (self), + self->frames->invocation, + FALSE, + self->frames->filename ?: ""); + return; + } + + submit_screenshot (self); +} + + +static void +screencopy_frame_handle_ready (void *data, + struct zwlr_screencopy_frame_v1 *frame, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec) +{ + ScreencopyFrame *screencopy_frame = data; + g_autoptr (GdkPixbuf) pixbuf = NULL; + g_autoptr (GBytes) bytes = NULL; + + if (screencopy_frame->monitor == NULL) { + g_warning ("Output went away during screenshot"); + screencopy_frame->state = FRAME_STATE_FAILURE; + goto out; + } + + g_debug ("Frame %p %dx%d, stride %d, format 0x%x for %s ready", + frame, + screencopy_frame->buffer->width, + screencopy_frame->buffer->height, + screencopy_frame->buffer->stride, + screencopy_frame->buffer->format, + screencopy_frame->monitor->name); + + switch ((uint32_t) screencopy_frame->buffer->format) { + case WL_SHM_FORMAT_ABGR8888: + case WL_SHM_FORMAT_XBGR8888: + break; + case WL_SHM_FORMAT_ARGB8888: + case WL_SHM_FORMAT_XRGB8888: { /* ARGB -> ABGR */ + PhoshWlBuffer *buffer = screencopy_frame->buffer; + uint8_t *d = buffer->data; + for (int i = 0; i < buffer->height; ++i) { + for (int j = 0; j < buffer->width; ++j) { + uint32_t *px = (uint32_t *)(d + i * buffer->stride + j * 4); + uint8_t a = (*px >> 24) & 0xFF; + uint8_t b = (*px >> 16) & 0xFF; + uint8_t g = (*px >> 8) & 0xFF; + uint8_t r = *px & 0xFF; + *px = (a << 24) | (r << 16) | (g << 8) | b; + } + } + if (buffer->format == WL_SHM_FORMAT_ARGB8888) + buffer->format = WL_SHM_FORMAT_ABGR8888; + else + buffer->format = WL_SHM_FORMAT_XBGR8888; + } + break; + default: + g_warning ("Unknown buffer formeat 0x%x on %s", + screencopy_frame->buffer->format, + screencopy_frame->monitor->name); + screencopy_frame->state = FRAME_STATE_FAILURE; + goto out; + } + + bytes = phosh_wl_buffer_get_bytes (screencopy_frame->buffer); + pixbuf = gdk_pixbuf_new_from_bytes (bytes, + GDK_COLORSPACE_RGB, + TRUE, + 8, + screencopy_frame->buffer->width, + screencopy_frame->buffer->height, + screencopy_frame->buffer->stride); + if (screencopy_frame->flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT) { + GdkPixbuf *tmp = gdk_pixbuf_flip (pixbuf, FALSE); + g_object_unref (pixbuf); + pixbuf = tmp; + } + + screencopy_frame->pixbuf = g_steal_pointer (&pixbuf); + screencopy_frame->state = FRAME_STATE_SUCCESS; + + out: + maybe_screencopy_done (screencopy_frame->manager); +} + + +static void +screencopy_frame_handle_failed (void *data, + struct zwlr_screencopy_frame_v1 *frame) +{ + ScreencopyFrame *screencopy_frame = data; + const char *name = screencopy_frame->monitor ? screencopy_frame->monitor->name : ""; + + screencopy_frame->state = FRAME_STATE_FAILURE; + g_warning ("Failed to copy output '%s'\n", name); + maybe_screencopy_done (screencopy_frame->manager); +} + + +static const struct zwlr_screencopy_frame_v1_listener screencopy_frame_listener = { + .buffer = screencopy_frame_handle_buffer, + .flags = screencopy_frame_handle_flags, + .ready = screencopy_frame_handle_ready, + .failed = screencopy_frame_handle_failed, +}; + + +/** + * build_dbus_filename: + * @pattern: Absolute path or relative name without extension + * + * Builds an absolute filename based on the given input pattern. + * Returns: The target filename or %NULL on errors. + */ +static char * +build_dbus_filename (const char *pattern) +{ + g_autofree char *filename = NULL; + + if (gm_str_is_null_or_empty (pattern)) + return NULL; + + if (g_path_is_absolute (pattern)) { + return g_strdup (pattern); + } else { + const char *dir = NULL; + const char *const *dirs = (const char * []) { + g_get_user_special_dir (G_USER_DIRECTORY_PICTURES), + g_get_home_dir (), + NULL + }; + + for (int i = 0; i < g_strv_length ((GStrv) dirs); i++) { + if (g_file_test (dirs[i], G_FILE_TEST_EXISTS)) { + dir = dirs[i]; + break; + } + } + + if (!dir) + return NULL; + + filename = g_build_filename (dir, pattern, NULL); + } + if (!g_str_has_suffix (filename, ".png")) + filename = g_strdup_printf ("%s.png", filename); + + return g_steal_pointer (&filename); +} + +/** + * phosh_screenshot_manager_do_screenshot: + * @self: The screenshot manager + * @area: (nullable): The area to capture or %NULL to capture all outputs + * @include_cursor: Whether to include the cursor + * + * Initiate a screenshot of all outputs or the given area. + * + * Returns: `FALSE` on failure, otherwise `TRUE` + */ +static gboolean +phosh_screenshot_manager_do_screenshot (PhoshScreenshotManager *self, + const GdkRectangle *area, + gboolean include_cursor) +{ + g_autoptr (ScreencopyFrames) frames = NULL; + PhoshMonitorManager *monitor_manager; + PhoshWayland *wl = phosh_wayland_get_default (); + float max_scale = 0.0; + int num_outputs = 0; + + monitor_manager = phosh_shell_get_monitor_manager (phosh_shell_get_default ()); + g_return_val_if_fail (PHOSH_IS_MONITOR_MANAGER (monitor_manager), -EPERM); + g_return_val_if_fail (PHOSH_IS_WAYLAND (wl), -EPERM); + + if (self->wl_scm == NULL) { + g_debug ("No screenshot support"); + return FALSE; + } + + if (self->frames) { + g_debug ("Screenshot already in progress"); + return FALSE; + } + + frames = g_new0 (ScreencopyFrames, 1); + frames->flash = TRUE; + + /* Determine which monitors are involved in the area we want to screenshot. */ + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (monitor_manager); i++) { + PhoshMonitor *monitor = phosh_monitor_manager_get_monitor (monitor_manager, i); + ScreencopyFrame *screencopy_frame; + GdkRectangle monitor_area; + float monitor_scale; + + if (monitor == NULL) + continue; + + if (area) { + monitor_area = (GdkRectangle) { + .x = monitor->logical.x, + .y = monitor->logical.y, + .width = monitor->logical.width, + .height = monitor->logical.height, + }; + if (gdk_rectangle_intersect (area, &monitor_area, NULL) == FALSE) + continue; + } + + screencopy_frame = g_new0 (ScreencopyFrame, 1); + screencopy_frame->manager = self; + screencopy_frame->monitor = monitor; + g_object_add_weak_pointer (G_OBJECT (monitor), (gpointer)&screencopy_frame->monitor); + screencopy_frame->frame = zwlr_screencopy_manager_v1_capture_output ( + self->wl_scm, include_cursor, monitor->wl_output); + zwlr_screencopy_frame_v1_add_listener (screencopy_frame->frame, &screencopy_frame_listener, + screencopy_frame); + frames->frames = g_list_prepend (frames->frames, screencopy_frame); + num_outputs++; + + /* Use the maximum scale of an involved monitor as the screenshot scale. */ + monitor_scale = phosh_monitor_get_fractional_scale (monitor); + max_scale = fmaxf (max_scale, monitor_scale); + } + frames->num_outputs = num_outputs; + + g_return_val_if_fail (max_scale > 0.0, FALSE); + frames->max_scale = max_scale; + + if (area) + frames->area = g_memdup2 (area, sizeof (GdkRectangle)); + + self->frames = g_steal_pointer (&frames); + return TRUE; +} + + +static gboolean +handle_screenshot_area (PhoshDBusScreenshot *object, + GDBusMethodInvocation *invocation, + gint arg_x, + gint arg_y, + gint arg_width, + gint arg_height, + gboolean arg_flash, + const char *arg_filename) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (object); + GdkRectangle area; + gboolean success; + + g_debug ("DBus call %s: @%d,%d %dx%d, flash %d, to %s", + __func__, arg_x, arg_y, arg_width, arg_height, arg_flash, arg_filename); + + area = (GdkRectangle) { + .x = arg_x, + .y = arg_y, + .width = arg_width, + .height = arg_height, + }; + + success = phosh_screenshot_manager_do_screenshot (self, &area, FALSE); + if (!success) { + phosh_dbus_screenshot_complete_screenshot_area (object, invocation, FALSE, ""); + return TRUE; + } + + self->frames->flash = arg_flash; + self->frames->invocation = invocation; + self->frames->filename = build_dbus_filename (arg_filename); + self->frames->copy_to_clipboard = !self->frames->filename; + + return TRUE; +} + + +static gboolean +handle_screenshot (PhoshDBusScreenshot *object, + GDBusMethodInvocation *invocation, + gboolean arg_include_cursor, + gboolean arg_flash, + const char *arg_filename) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (object); + gboolean success; + + g_debug ("DBus call %s, cursor: %d, flash %d, to %s", + __func__, arg_include_cursor, arg_flash, arg_filename); + + success = phosh_screenshot_manager_do_screenshot (self, NULL, arg_include_cursor); + if (!success) { + phosh_dbus_screenshot_complete_screenshot (object, invocation, FALSE, ""); + return TRUE; + } + + self->frames->flash = arg_flash; + self->frames->invocation = invocation; + self->frames->filename = build_dbus_filename (arg_filename); + self->frames->copy_to_clipboard = !self->frames->filename; + + return TRUE; +} + + +/* Taken from grim */ +static gboolean +parse_slurp (const char *str, GdkRectangle *box) +{ + char *end = NULL; + char *next; + + box->x = strtol (str, &end, 10); + if (end[0] != ',') + return FALSE; + + next = end + 1; + box->y = strtol (next, &end, 10); + if (end[0] != ' ') + return FALSE; + + next = end + 1; + box->width = strtol (next, &end, 10); + if (end[0] != 'x') + return FALSE; + + next = end + 1; + box->height = strtol (next, &end, 10); + if (end[0] != '\0' && end[0] != '\n') + return FALSE; + + return TRUE; +} + + +static void +on_slurp_exited (GPid pid, int wait_status, gpointer user_data) +{ + PhoshScreenshotManager *self; + GdkRectangle box; + + g_return_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (user_data)); + self = PHOSH_SCREENSHOT_MANAGER (user_data); + + g_return_if_fail (pid == self->slurp->pid); + + g_debug ("Selected area: %s", self->slurp->response->str); + + if (parse_slurp (self->slurp->response->str, &box)) { + phosh_dbus_screenshot_complete_select_area (PHOSH_DBUS_SCREENSHOT (self), + self->slurp->invocation, + box.x, box.y, box.width, box.height); + } else { + g_dbus_method_invocation_return_error (self->slurp->invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Area selection failed"); + } + + g_clear_pointer (&self->slurp, slurp_area_dispose); +} + + +static void +on_slurp_read_done (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + GInputStream *slurp_out = G_INPUT_STREAM (source_object); + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (user_data); + gssize read_size; + g_autoptr (GError) error = NULL; + + read_size = g_input_stream_read_finish (slurp_out, res, &error); + switch (read_size) { + case -1: + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) { + g_warning ("Slurp cancelled"); + g_dbus_method_invocation_return_error (self->slurp->invocation, G_DBUS_ERROR, + G_DBUS_ERROR_FAILED, + "Area selection cancelled"); + g_clear_pointer (&self->slurp, slurp_area_dispose); + return; + } + break; + case 0: + /* Done reading. */ + self->slurp->child_watch_id = g_child_watch_add (self->slurp->pid, on_slurp_exited, self); + break; + default: + g_string_append_len (self->slurp->response, self->slurp->read_buf, read_size); + g_input_stream_read_async (slurp_out, + self->slurp->read_buf, + sizeof(self->slurp->read_buf), + G_PRIORITY_DEFAULT, + NULL, + on_slurp_read_done, + self); + return; + } + + g_input_stream_close (slurp_out, NULL, NULL); +} + + +static gboolean +handle_select_area (PhoshDBusScreenshot *object, + GDBusMethodInvocation *invocation) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (object); + const char *cmd[] = { "slurp", NULL }; + gboolean success; + int slurp_stdout_fd; + g_autofree SlurpArea *slurp = NULL; + g_autoptr (GError) err = NULL; + GPid slurp_pid; + + g_debug ("DBus call %s", __func__); + + g_return_val_if_fail (slurp == NULL, FALSE); + + success = g_spawn_async_with_pipes (NULL, + (char **) cmd, + NULL, /* envp */ + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, /* setup-func */ + NULL, /* user_data */ + &slurp_pid, + NULL, + &slurp_stdout_fd, + NULL, + &err); + if (!success) { + g_warning ("Failed to spawn slurp: %s", err->message); + return FALSE; + } + + slurp = g_new0 (SlurpArea, 1); + slurp->stdout_ = g_unix_input_stream_new (slurp_stdout_fd, TRUE); + slurp->invocation = invocation; + slurp->pid = slurp_pid; + slurp->response = g_string_new (NULL); + g_input_stream_read_async (slurp->stdout_, + slurp->read_buf, + sizeof(slurp->read_buf), + G_PRIORITY_DEFAULT, + slurp->cancel, + on_slurp_read_done, + self); + + self->slurp = g_steal_pointer (&slurp); + return TRUE; +} + + +static void +phosh_screenshot_manager_screenshot_iface_init (PhoshDBusScreenshotIface *iface) +{ + iface->handle_screenshot = handle_screenshot; + iface->handle_select_area = handle_select_area; + iface->handle_screenshot_area = handle_screenshot_area; +} + + +static void +take_screenshot (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (data); + + g_return_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (self)); + + phosh_screenshot_manager_take_screenshot (self, NULL, NULL, TRUE, FALSE); +} + + +static void +add_keybindings (PhoshScreenshotManager *self) +{ + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + g_autoptr (GArray) actions = g_array_new (FALSE, TRUE, sizeof (GActionEntry)); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->settings, + KEYBINDING_KEY_SCREENSHOT, + take_screenshot); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry *)actions->data, + actions->len, + self); + self->action_names = g_strv_builder_end (builder); +} + + +static void +on_keybindings_changed (PhoshScreenshotManager *self) +{ + g_debug ("Updating keybindings in screenshot-manager"); + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + add_keybindings (self); +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (user_data); + + g_return_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (self)); + g_debug ("Acquired name %s", name); +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("Lost or failed to acquire name %s", name); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + OBJECT_PATH, + &err)) { + g_warning ("Failed to export screensaver interface skeleton: %s", err->message); + } + +} + + +static void +phosh_screenshot_manager_constructed (GObject *object) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (object); + PhoshWayland *wl = phosh_wayland_get_default (); + + G_OBJECT_CLASS (phosh_screenshot_manager_parent_class)->constructed (object); + + self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT | + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + self, + NULL); + + g_return_if_fail (PHOSH_IS_WAYLAND (wl)); + self->wl_scm = phosh_wayland_get_zwlr_screencopy_manager_v1 (wl); +} + + +static void +phosh_screenshot_manager_dispose (GObject *object) +{ + PhoshScreenshotManager *self = PHOSH_SCREENSHOT_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_pointer (&self->frames, screencopy_frames_dispose); + g_clear_object (&self->for_clipboard); + g_clear_pointer (&self->slurp, slurp_area_dispose); + + g_clear_handle_id (&self->fader_id, g_source_remove); + g_clear_handle_id (&self->opaque_id, g_source_remove); + g_clear_pointer (&self->fader, phosh_cp_widget_destroy); + + g_clear_pointer (&self->action_names, g_strfreev); + g_clear_object (&self->settings); + + G_OBJECT_CLASS (phosh_screenshot_manager_parent_class)->dispose (object); +} + + +static void +phosh_screenshot_manager_class_init (PhoshScreenshotManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_screenshot_manager_constructed; + object_class->dispose = phosh_screenshot_manager_dispose; +} + + +static void +phosh_screenshot_manager_init (PhoshScreenshotManager *self) +{ + self->cancel = g_cancellable_new (); + self->settings = g_settings_new (KEYBINDINGS_SCHEMA_ID); + g_signal_connect_swapped (self->settings, + "changed::" KEYBINDING_KEY_SCREENSHOT, + G_CALLBACK (on_keybindings_changed), + self); + add_keybindings (self); +} + +PhoshScreenshotManager * +phosh_screenshot_manager_new (void) +{ + return PHOSH_SCREENSHOT_MANAGER (g_object_new (PHOSH_TYPE_SCREENSHOT_MANAGER, NULL)); +} + +/** + * phosh_screenshot_manager_take_screenshot: + * @self: The screenshot manager + * @area: (nullable): The area to capture or %NULL to capture all outputs + * @filename: (nullable): The output filename or %NULL to autogenerate a filename + * @copy_to_clipboard: Whether to use the clipboard + * @include_cursor: Whether to include the cursor + * + * Initiate a screenshot of all outputs or the given area. If `copy_to_clipboard` is + * `TRUE` the screenshot is also copied to the clipboard. + * + * Returns: `FALSE` on failure, otherwise `TRUE` + */ +gboolean +phosh_screenshot_manager_take_screenshot (PhoshScreenshotManager *self, + const GdkRectangle *area, + const char *filename, + gboolean copy_to_clipboard, + gboolean include_cursor) +{ + gboolean ret; + + ret = phosh_screenshot_manager_do_screenshot (self, area, include_cursor); + self->frames->copy_to_clipboard = copy_to_clipboard; + + return ret; +} diff --git a/src/screenshot-manager.h b/src/screenshot-manager.h new file mode 100644 index 000000000..f9c6129c6 --- /dev/null +++ b/src/screenshot-manager.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "dbus/phosh-screenshot-dbus.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SCREENSHOT_MANAGER phosh_screenshot_manager_get_type () + +G_DECLARE_FINAL_TYPE (PhoshScreenshotManager, phosh_screenshot_manager, + PHOSH, SCREENSHOT_MANAGER, PhoshDBusScreenshotSkeleton) + +PhoshScreenshotManager *phosh_screenshot_manager_new (void); +gboolean phosh_screenshot_manager_take_screenshot (PhoshScreenshotManager *self, + const GdkRectangle *area, + const char *filename, + gboolean copy_to_clipboard, + gboolean include_cursor); + +G_END_DECLS diff --git a/src/search/meson.build b/src/search/meson.build new file mode 100644 index 000000000..3a255f5dd --- /dev/null +++ b/src/search/meson.build @@ -0,0 +1,31 @@ +phosh_search_sources = [] + +phoshsearch_sources = [ + 'search-client.c', + 'search-client.h', + 'search-result-meta.c', + 'search-result-meta.h', + 'search-source.c', + 'search-source.h', + generated_dbus_sources, + generated_dbus_headers, +] + +phosh_search_deps = [gio_dep, gio_unix_dep, dependency('gdk-pixbuf-2.0')] + +phosh_search_lib = static_library( + 'phoshsearch', + phoshsearch_sources, + include_directories: [root_inc, dbus_inc], + dependencies: phosh_search_deps, + install_dir: pkglibdir, + install: false, +) +phosh_search_inc = include_directories('.') + +phosh_search_dep = declare_dependency( + sources: generated_dbus_sources + generated_dbus_headers, + include_directories: [dbus_inc, phosh_search_inc], + link_with: phosh_search_lib, + dependencies: phosh_search_deps, +) diff --git a/src/search/search-client.c b/src/search/search-client.c new file mode 100644 index 000000000..25bf29932 --- /dev/null +++ b/src/search/search-client.c @@ -0,0 +1,541 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Zander Brown + * Gotam Gorabh + */ + +#include "search-client.h" +#include "search-result-meta.h" +#include "search-source.h" +#include "phosh-searchd.h" + +/** + * PhoshSearchClient: + * + * Client for interfacing with the Phosh search service + * + * The #PhoshSearchClient class provides an interface to interact with the + * Phosh search service over D-Bus. It allows client to query for results + * from different search sources. + */ + +enum State { + CREATED, + STARTING, + READY, +}; + +enum { + SOURCE_RESULTS_CHANGED, + QUERY_FINISHED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +typedef struct _PhoshSearchClientPrivate PhoshSearchClientPrivate; +struct _PhoshSearchClientPrivate { + PhoshDBusSearch *server; + + enum State state; + + GRegex *highlight; + GRegex *splitter; + + GCancellable *cancellable; +}; + +static void async_iface_init (GAsyncInitableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSearchClient, phosh_search_client, G_TYPE_OBJECT, + G_ADD_PRIVATE (PhoshSearchClient) + G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, async_iface_init)) + + +static void +phosh_search_client_finalize (GObject *object) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (object); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + + g_cancellable_cancel (priv->cancellable); + + g_clear_object (&priv->server); + g_clear_object (&priv->cancellable); + + g_clear_pointer (&priv->highlight, g_regex_unref); + g_clear_pointer (&priv->splitter, g_regex_unref); + + G_OBJECT_CLASS (phosh_search_client_parent_class)->finalize (object); +} + + +static void +phosh_search_client_class_init (PhoshSearchClientClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_search_client_finalize; + + signals[SOURCE_RESULTS_CHANGED] = g_signal_new ("source-results-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_DETAILED, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 2, + G_TYPE_STRING, + G_TYPE_PTR_ARRAY); + + signals[QUERY_FINISHED] = g_signal_new ("query-finished", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 0); +} + + +static void +on_source_results_changed (PhoshDBusSearch *server, + const char *source_id, + GVariant *variant, + PhoshSearchClient *self) +{ + GVariantIter iter; + GVariant *item; + GPtrArray *results = NULL; + + results = g_ptr_array_new_with_free_func ((GDestroyNotify) phosh_search_result_meta_unref); + + g_variant_iter_init (&iter, variant); + while ((item = g_variant_iter_next_value (&iter))) { + g_autoptr (PhoshSearchResultMeta) result = NULL; + + result = phosh_search_result_meta_deserialise (item); + + g_ptr_array_add (results, phosh_search_result_meta_ref (result)); + + g_clear_pointer (&item, g_variant_unref); + } + + g_signal_emit (self, + signals[SOURCE_RESULTS_CHANGED], + g_quark_from_string (source_id), + source_id, + results); +} + + +static void +on_query_finished (PhoshDBusSearch *server, gpointer user_data) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (user_data); + + g_signal_emit (self, signals[QUERY_FINISHED], 0); +} + + +struct InitData { + PhoshSearchClient *self; + GTask *task; +}; + +static void +got_search (GObject *source, GAsyncResult *result, gpointer user_data) +{ + g_autoptr (GError) error = NULL; + g_autofree struct InitData *data = user_data; + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (data->self); + + priv->server = phosh_dbus_search_proxy_new_finish (result, &error); + + priv->state = READY; + + if (!priv->server) { + g_task_return_error (data->task, error); + return; + } + + g_signal_connect (priv->server, "source-results-changed", G_CALLBACK (on_source_results_changed), data->self); + g_signal_connect (priv->server, "query-finished", G_CALLBACK (on_query_finished), data->self); + + g_task_return_boolean (data->task, TRUE); +} + + +static void +phosh_search_client_init_async (GAsyncInitable *initable, + int io_priority, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (initable); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + GTask* task = NULL; + struct InitData *data; + + switch (priv->state) { + case CREATED: + priv->state = STARTING; + + task = g_task_new (initable, cancellable, callback, user_data); + + data = g_new0 (struct InitData, 1); + data->self = self; + data->task = task; + + phosh_dbus_search_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "mobi.phosh.Shell.Search", + "/mobi/phosh/Shell/Search", + cancellable, + got_search, + data); + break; + case STARTING: + g_critical ("Already initialising"); + break; + case READY: + g_critical ("Already initialised"); + break; + default: + g_assert_not_reached (); + } +} + + +static gboolean +phosh_search_client_init_finish (GAsyncInitable *initable, GAsyncResult *result, GError **error) +{ + g_return_val_if_fail (g_task_is_valid (result, initable), FALSE); + + return g_task_propagate_boolean (G_TASK (result), error); +} + + +static void +async_iface_init (GAsyncInitableIface *iface) +{ + iface->init_async = phosh_search_client_init_async; + iface->init_finish = phosh_search_client_init_finish; +} + + +static void +phosh_search_client_init (PhoshSearchClient *self) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + + priv->highlight = NULL; + priv->splitter = g_regex_new ("\\s+", + G_REGEX_CASELESS | G_REGEX_MULTILINE, + 0, + &error); + priv->state = CREATED; + priv->cancellable = g_cancellable_new (); + + if (error) + g_error ("Bad Regex: %s", error->message); +} + + +static void +got_query (GObject *source, GAsyncResult *result, gpointer user_data) +{ + g_autoptr (GTask) task = user_data; + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (g_task_get_source_object (task)); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + gboolean out_searching; + gboolean success; + + success = phosh_dbus_search_call_query_finish (priv->server, &out_searching, result, &error); + + if (!success) { + g_critical ("Unable to send search: %s", error->message); + g_task_return_error (task, error); + } + + g_task_return_boolean (task, out_searching); +} + + +void +phosh_search_client_query (PhoshSearchClient *self, + const char *query, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autofree char *regex_terms = NULL; + g_autofree char *regex = NULL; + g_autofree char *striped = NULL; + g_auto (GStrv) parts = NULL; + g_auto (GStrv) escaped = NULL; + g_autoptr (GError) error = NULL; + GTask *task = NULL; + int len = 0; + int i = 0; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_client_query); + + phosh_dbus_search_call_query (priv->server, query, priv->cancellable, got_query, task); + + striped = g_strstrip (g_strdup (query)); + parts = g_regex_split (priv->splitter, striped, 0); + + len = parts ? g_strv_length (parts) : 0; + + escaped = g_new0 (char *, len + 1); + + while (parts[i]) { + escaped[i] = g_regex_escape_string (parts[i], -1); + + i++; + } + escaped[len] = NULL; + + regex_terms = g_strjoinv ("|", escaped); + regex = g_strconcat ("(", regex_terms, ")", NULL); + priv->highlight = g_regex_new (regex, G_REGEX_CASELESS | G_REGEX_MULTILINE, 0, &error); + + if (error) { + g_warning ("Unable to prepare highlighter: %s", error->message); + g_clear_error (&error); + priv->highlight = NULL; + } +} + + +gboolean +phosh_search_client_query_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_client_query); + + + return g_task_propagate_boolean (G_TASK (res), error); +} + + +char * +phosh_search_client_markup_string (PhoshSearchClient *self, const char *string) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + char *marked = NULL; + + if (string == NULL) + return NULL; + + marked = g_regex_replace (priv->highlight, string, -1, 0, "\\1", 0, &error); + + if (error) + return g_strdup (string); + + return marked; +} + + +void +phosh_search_client_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_async_initable_new_async (PHOSH_TYPE_SEARCH_CLIENT, + G_PRIORITY_DEFAULT, + cancellable, + callback, + user_data, + NULL); +} + + +PhoshSearchClient * +phosh_search_client_new_finish (GObject *source, GAsyncResult *result, GError **error) +{ + return PHOSH_SEARCH_CLIENT (g_async_initable_new_finish (G_ASYNC_INITABLE (source), + result, + error)); +} + + +static void +got_last_results (GObject *source, GAsyncResult *result, gpointer user_data) +{ + g_autoptr (GTask) task = user_data; + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (g_task_get_source_object (task)); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) results = NULL; + gboolean success; + + success = phosh_dbus_search_call_get_last_results_finish (priv->server, + &results, + result, + &error); + + if (!success) { + g_critical ("Unable to get last results: %s", error->message); + g_task_return_error (task, error); + return; + } + + g_task_return_pointer (task, g_variant_ref (results), (GDestroyNotify) g_variant_unref); +} + + +void +phosh_search_client_get_last_results (PhoshSearchClient *self, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + GTask *task; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_client_get_last_results); + + phosh_dbus_search_call_get_last_results (priv->server, + priv->cancellable, + got_last_results, + task); +} + + +GVariant * +phosh_search_client_get_last_results_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_client_get_last_results); + + return g_task_propagate_pointer (G_TASK (res), error); +} + + +static void +got_called_activate_result (GObject *source_object, + GAsyncResult *async_result, + gpointer user_data) +{ + g_autoptr (GTask) task = user_data; + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (g_task_get_source_object (task)); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + gboolean success; + + success = phosh_dbus_search_call_activate_result_finish (priv->server, async_result, &error); + + if (!success) { + g_critical ("%s: Could not activate result for this request.\n", error->message); + g_task_return_error (task, error); + } + + g_task_return_boolean (task, success); +} + + +void +phosh_search_client_activate_result (PhoshSearchClient *self, + const char *source_id, + const char *result_id, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + GDateTime *dt = g_date_time_new_now_utc (); + GTask *task = NULL; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_client_activate_result); + + phosh_dbus_search_call_activate_result (priv->server, + source_id, + result_id, + (int) g_date_time_get_seconds (dt), + priv->cancellable, + got_called_activate_result, + task); +} + + +gboolean +phosh_search_client_activate_result_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_client_activate_result); + + return g_task_propagate_boolean (G_TASK (res), error); +} + + +static void +got_called_launch_source (GObject *source_object, + GAsyncResult *async_result, + gpointer user_data) +{ + g_autoptr (GTask) task = user_data; + PhoshSearchClient *self = PHOSH_SEARCH_CLIENT (g_task_get_source_object (task)); + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + g_autoptr (GError) error = NULL; + gboolean success; + + success = phosh_dbus_search_call_launch_source_finish (priv->server, async_result, &error); + + if (!success) { + g_critical ("%s: Could not launch search for this request.\n", error->message); + g_task_return_error (task, error); + } + + g_task_return_boolean (task, success); + +} + + +void +phosh_search_client_launch_source (PhoshSearchClient *self, + const char *source_id, + GAsyncReadyCallback callback, + gpointer callback_data) +{ + PhoshSearchClientPrivate *priv = phosh_search_client_get_instance_private (self); + GDateTime *dt = g_date_time_new_now_utc (); + GTask *task = NULL; + + task = g_task_new (self, priv->cancellable, callback, callback_data); + g_task_set_source_tag (task, phosh_search_client_launch_source); + + phosh_dbus_search_call_launch_source (priv->server, + source_id, + (int) g_date_time_get_seconds (dt), + priv->cancellable, + got_called_launch_source, + task); +} + + +gboolean +phosh_search_client_launch_source_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error) +{ + g_assert_true (g_task_is_valid (res, self)); + g_assert_true (g_task_get_source_tag (G_TASK (res)) == phosh_search_client_launch_source); + + return g_task_propagate_boolean (G_TASK (res), error); +} + diff --git a/src/search/search-client.h b/src/search/search-client.h new file mode 100644 index 000000000..2f3dd9700 --- /dev/null +++ b/src/search/search-client.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2019 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_CLIENT phosh_search_client_get_type() +G_DECLARE_DERIVABLE_TYPE (PhoshSearchClient, phosh_search_client, PHOSH, SEARCH_CLIENT, GObject) + +struct _PhoshSearchClientClass +{ + GObjectClass parent_class; +}; + +char *phosh_search_client_markup_string (PhoshSearchClient *self, + const char *string); +void phosh_search_client_new (GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +PhoshSearchClient *phosh_search_client_new_finish (GObject *source, + GAsyncResult *result, + GError **error); +void phosh_search_client_query (PhoshSearchClient *self, + const char *query, + GAsyncReadyCallback callback, + gpointer callback_data); +gboolean phosh_search_client_query_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error); +void phosh_search_client_get_last_results (PhoshSearchClient *self, + GAsyncReadyCallback callback, + gpointer callback_data); +GVariant *phosh_search_client_get_last_results_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error); +void phosh_search_client_activate_result (PhoshSearchClient *self, + const char *source_id, + const char *result_id, + GAsyncReadyCallback callback, + gpointer callback_data); +gboolean phosh_search_client_activate_result_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error); +void phosh_search_client_launch_source (PhoshSearchClient *self, + const char *source_id, + GAsyncReadyCallback callback, + gpointer callback_data); +gboolean phosh_search_client_launch_source_finish (PhoshSearchClient *self, + GAsyncResult *res, + GError **error); + +G_END_DECLS diff --git a/src/search/search-result-meta.c b/src/search/search-result-meta.c new file mode 100644 index 000000000..4c0a384d4 --- /dev/null +++ b/src/search/search-result-meta.c @@ -0,0 +1,280 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#define G_LOG_DOMAIN "phosh-search-result-meta" + +#include "search-result-meta.h" + + +/** + * PhoshSearchResultMeta: + * + * A low level immutable representation of a search result + * + * It is a ref-counted ( g_rc_box_alloc0() ) #GBoxed type (rather than a full + * #GObject) as a large number may be created and destroyed during a search, + * when the data is immutable the overhead of #GObject is unnecessary + * + * A #PhoshSearchResultMeta can be serialised to a #GVariant for transport + * over dbus + * + * #PhoshSearchResult provides a #GObject wrapper + */ + + +struct _PhoshSearchResultMeta { + char *id; + char *title; + char *desc; + char *clipboard_text; + GIcon *icon; +}; + + +/** + * phosh_search_result_meta_ref: + * @self: A #PhoshSearchResultMeta instance. + * + * Increase the reference count of @self + * + * Returns: (transfer full): @self + */ +PhoshSearchResultMeta * +phosh_search_result_meta_ref (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return g_rc_box_acquire (self); +} + + +static void +result_free (gpointer source) +{ + PhoshSearchResultMeta *self = source; + + g_clear_pointer (&self->id, g_free); + g_clear_pointer (&self->title, g_free); + g_clear_pointer (&self->desc, g_free); + g_clear_object (&self->icon); + g_clear_pointer (&self->clipboard_text, g_free); +} + +/** + * phosh_search_result_meta_unref: + * @self: A #PhoshSearchResultMeta instance. + * + * Decrease the reference count of @self, potentially destroying it + */ +void +phosh_search_result_meta_unref (PhoshSearchResultMeta *self) +{ + g_return_if_fail (self != NULL); + + g_rc_box_release_full (self, result_free); +} + + +G_DEFINE_BOXED_TYPE (PhoshSearchResultMeta, + phosh_search_result_meta, + phosh_search_result_meta_ref, + phosh_search_result_meta_unref) + + +/** + * phosh_search_result_meta_get_id: + * @self: A #PhoshSearchResultMeta instance. + * + * Get the result id, note this **should** be unique withing the source of @self + * but this is not guaranteed + * + * Returns: (transfer none): the result "id" of @self + */ +const char * +phosh_search_result_meta_get_id (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->id; +} + +/** + * phosh_search_result_meta_get_title: + * @self: A #PhoshSearchResultMeta instance. + * + * Get the result title + * + * Returns: (transfer none): the title of @self + */ +const char * +phosh_search_result_meta_get_title (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->title; +} + +/** + * phosh_search_result_meta_get_description: + * @self: A #PhoshSearchResultMeta instance. + * + * Get the result description, this optional text providing further + * information about @self + * + * Returns: (transfer none) (nullable): the description of @self + */ +const char * +phosh_search_result_meta_get_description (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->desc; +} + +/** + * phosh_search_result_meta_get_clipboard_text: + * @self: A #PhoshSearchResultMeta instance. + * + * Get the clipboard text, this is a string that should be sent to the + * clipboard when the user activates @self + * + * NOTE: This is rarely provided + * + * Returns: (transfer none) (nullable): the clipboard text of @self + */ +const char * +phosh_search_result_meta_get_clipboard_text (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->clipboard_text; +} + +/** + * phosh_search_result_meta_get_icon: + * @self: A #PhoshSearchResultMeta instance. + * + * Get the icon, this is a small image representing @self (such as an app icon + * or file thumbnail) + * + * Returns: (transfer none) (nullable): the icon of @self + */ +GIcon * +phosh_search_result_meta_get_icon (PhoshSearchResultMeta *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->icon; +} + +/** + * phosh_search_result_meta_new: + * @id: the result id + * @title: the title + * @desc: (optional): the description + * @icon: (optional): the icon + * @clipboard: (optional): the clipboard text + * + * Construct a new #PhoshSearchResultMeta for the given data + * + * The @icon #GIcon implementation **must** support g_icon_serialize() + * + * Returns: (transfer full): the new #PhoshSearchResultMeta + */ +PhoshSearchResultMeta * +phosh_search_result_meta_new (const char *id, + const char *title, + const char *desc, + GIcon *icon, + const char *clipboard) +{ + PhoshSearchResultMeta *self = g_rc_box_new0 (PhoshSearchResultMeta); + + self->id = g_strdup (id); + self->title = g_strdup (title); + self->desc = g_strdup (desc); + g_set_object (&self->icon, icon); + self->clipboard_text = g_strdup (clipboard); + + return self; +} + +/** + * phosh_search_result_meta_serialise: + * @self: tA #PhoshSearchResultMeta instance. + * + * Generate a #GVariant representation of @self suitable for transport over + * dbus, the exact format is not defined and may change - the variant should + * only be interpreted with phosh_search_result_meta_deserialise() + * + * Returns: (transfer full): a #GVariant representing @self + */ +GVariant * +phosh_search_result_meta_serialise (PhoshSearchResultMeta *self) +{ + g_autoptr (GVariant) icon = NULL; + GVariantDict dict; + + g_variant_dict_init (&dict, NULL); + + g_variant_dict_insert (&dict, "id", "s", self->id); + g_variant_dict_insert (&dict, "title", "s", self->title); + if (self->desc) + g_variant_dict_insert (&dict, "desc", "s", self->desc); + + if (self->clipboard_text) + g_variant_dict_insert (&dict, "clipboard-text", "s", self->clipboard_text); + + if (self->icon) { + icon = g_icon_serialize (self->icon); + if (icon) + g_variant_dict_insert_value (&dict, "icon", icon); + else + g_warning ("Can't serialise icon of type %s", G_OBJECT_TYPE_NAME (self->icon)); + } + + return g_variant_dict_end (&dict); +} + +/** + * phosh_search_result_meta_deserialise: + * @variant: The #GVariant to deserialise + * + * Convert a #GVariant from phosh_search_result_meta_serialise() back to it's + * #PhoshSearchResultMeta, or %NULL for invalid input + * + * Returns: (transfer full) (nullable): the deserialised #PhoshSearchResultMeta + * or %NULL on failure + */ +PhoshSearchResultMeta * +phosh_search_result_meta_deserialise (GVariant *variant) +{ + PhoshSearchResultMeta *self = NULL; + g_autofree char *id = NULL; + g_autofree char *title = NULL; + g_autofree char *desc = NULL; + g_autoptr (GVariant) icon_data = NULL; + g_autofree char *clipboard_text = NULL; + g_autoptr (GIcon) icon = NULL; + g_autoptr (GVariantDict) dict = NULL; + + dict = g_variant_dict_new (variant); + + g_variant_dict_lookup (dict, "id", "s", &id); + g_variant_dict_lookup (dict, "title", "s", &title); + g_variant_dict_lookup (dict, "desc", "s", &desc); + g_variant_dict_lookup (dict, "clipboard-text", "s", &clipboard_text); + icon_data = g_variant_dict_lookup_value (dict, "icon", G_VARIANT_TYPE_ANY); + + if (icon_data) + icon = g_icon_deserialize (icon_data); + + self = phosh_search_result_meta_new (id, title, desc, icon, clipboard_text); + + return self; +} diff --git a/src/search/search-result-meta.h b/src/search/search-result-meta.h new file mode 100644 index 000000000..ebd0bc3c0 --- /dev/null +++ b/src/search/search-result-meta.h @@ -0,0 +1,39 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_RESULT_META (phosh_search_result_meta_get_type ()) + +typedef struct _PhoshSearchResultMeta PhoshSearchResultMeta; + +PhoshSearchResultMeta *phosh_search_result_meta_new (const char *id, + const char *title, + const char *description, + GIcon *icon, + const char *clipboard); +GType phosh_search_result_meta_get_type (void); +PhoshSearchResultMeta *phosh_search_result_meta_ref (PhoshSearchResultMeta *self); +void phosh_search_result_meta_unref (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_id (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_title (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_description (PhoshSearchResultMeta *self); +GIcon *phosh_search_result_meta_get_icon (PhoshSearchResultMeta *self); +const char *phosh_search_result_meta_get_clipboard_text (PhoshSearchResultMeta *self); +GVariant *phosh_search_result_meta_serialise (PhoshSearchResultMeta *self); +PhoshSearchResultMeta *phosh_search_result_meta_deserialise (GVariant *variant); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshSearchResultMeta, + phosh_search_result_meta_unref) + +G_END_DECLS diff --git a/src/search/search-source.c b/src/search/search-source.c new file mode 100644 index 000000000..589bfc695 --- /dev/null +++ b/src/search/search-source.c @@ -0,0 +1,205 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include + +#include "search-source.h" + + +/** + * PhoshSearchSource: + * + * A ref-counted (#g_rc_box_alloc0()) #GBoxed type containing information + * about a search source. + * + * #PhoshSearchSource provides details about a specific search source. + */ + + +struct _PhoshSearchSource { + char *id; + GAppInfo *app_info; + guint position; +}; + + +G_DEFINE_BOXED_TYPE (PhoshSearchSource, + phosh_search_source, + phosh_search_source_ref, + phosh_search_source_unref) + + +/** + * phosh_search_source_ref: + * @self: the #PhoshSearchSource + * + * Increase the reference count of @self + * + * Returns: (transfer full): @self + */ +PhoshSearchSource * +phosh_search_source_ref (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return g_rc_box_acquire (self); +} + + +static void +search_source_free (gpointer source) +{ + PhoshSearchSource *self = source; + + g_clear_pointer (&self->id, g_free); + g_clear_object (&self->app_info); +} + +/** + * phosh_search_source_unref: + * @self: the #PhoshSearchSource + * + * Decrease the reference count of @self, potentially destroying it + */ +void +phosh_search_source_unref (PhoshSearchSource *self) +{ + g_return_if_fail (self != NULL); + + g_rc_box_release_full (self, search_source_free); +} + +/** + * phosh_search_source_new: + * @id: the source id (currently the bus path, DO NOT rely on this) + * @app_info: #GAppInfo with information about the source + * + * Creat a new #PhoshSearchSource with the given @app_info and @id + * + * Returns: (transfer full): the new #PhoshSearchSource + */ +PhoshSearchSource * +phosh_search_source_new (const char *id, GAppInfo *app_info) +{ + PhoshSearchSource *self = g_rc_box_new0 (PhoshSearchSource); + + self->id = g_strdup (id); + g_set_object (&self->app_info, app_info); + self->position = 0; + + return self; +} + +/** + * phosh_search_source_get_id: + * @self: the #PhoshSearchSource + * + * Get the unique id of the source + * + * Returns: (transfer none): the source id of @self + */ +const char * +phosh_search_source_get_id (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->id; +} + +/** + * phosh_search_source_get_app_info: + * @self: the #PhoshSearchSource + * + * Get the #GAppInfo associated with the source, this contains the name + * and #GIcon of the source + * + * Returns: (transfer none): the app info of @self + */ +GAppInfo * +phosh_search_source_get_app_info (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, NULL); + + return self->app_info; +} + +/** + * phosh_search_source_get_position: + * @self: the #PhoshSearchSource + * + * Get the sort position of the source + * + * Returns: the sort position of @self + */ +guint +phosh_search_source_get_position (PhoshSearchSource *self) +{ + g_return_val_if_fail (self != NULL, 0); + + return self->position; +} + +/** + * phosh_search_source_set_position: + * @self: the #PhoshSearchSource + * @position: the sort position of the index + * + * The is only for use in the search daemon + */ +void +phosh_search_source_set_position (PhoshSearchSource *self, guint position) +{ + g_return_if_fail (self != NULL); + + self->position = position; +} + +/** + * phosh_search_source_serialise: + * @self: the #PhoshSearchSource + * + * Generate a #GVariant representation of @self suitable for transport over + * dbus, the exact format is not defined and may change - the variant should + * only be interpreted with phosh_search_source_deserialise() + * + * Returns: (transfer full): a #GVariant representing @self + */ +GVariant * +phosh_search_source_serialise (PhoshSearchSource *self) +{ + return g_variant_new ("(ssu)", self->id, g_app_info_get_id (self->app_info), self->position); +} + +/** + * phosh_search_source_deserialise: + * @variant: the #GVariant to deserialise + * + * Convert a #GVariant from phosh_search_source_serialise() back to it's + * #PhoshSearchSource, or %NULL for invalid input + * + * Returns: (transfer full) (nullable): the deserialised #PhoshSearchSource + * or %NULL on failure + */ +PhoshSearchSource * +phosh_search_source_deserialise (GVariant *variant) +{ + PhoshSearchSource *self = NULL; + g_autofree char *id = NULL; + g_autofree char *app_info_id = NULL; + g_autoptr (GAppInfo) app_info = NULL; + guint position; + + g_variant_get (variant, "(ssu)", &id, &app_info_id, &position); + + app_info = G_APP_INFO (g_desktop_app_info_new (app_info_id)); + + self = phosh_search_source_new (id, app_info); + phosh_search_source_set_position (self, position); + + return self; +} diff --git a/src/search/search-source.h b/src/search/search-source.h new file mode 100644 index 000000000..ad9be4fdc --- /dev/null +++ b/src/search/search-source.h @@ -0,0 +1,35 @@ +/* + * Copyright © 2019-2020 Zander Brown + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Zander Brown + */ + +#include +#include + +#pragma once + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SEARCH_SOURCE (phosh_search_source_get_type ()) + +typedef struct _PhoshSearchSource PhoshSearchSource; + +PhoshSearchSource *phosh_search_source_new (const char *id, + GAppInfo *app_info); +GType phosh_search_source_get_type (void); +PhoshSearchSource *phosh_search_source_ref (PhoshSearchSource *self); +void phosh_search_source_unref (PhoshSearchSource *self); +const char *phosh_search_source_get_id (PhoshSearchSource *self); +GAppInfo *phosh_search_source_get_app_info (PhoshSearchSource *self); +guint phosh_search_source_get_position (PhoshSearchSource *self); +void phosh_search_source_set_position (PhoshSearchSource *self, + guint position); +GVariant *phosh_search_source_serialise (PhoshSearchSource *self); +PhoshSearchSource *phosh_search_source_deserialise (GVariant *variant); + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (PhoshSearchSource, phosh_search_source_unref) + +G_END_DECLS diff --git a/src/sensor-proxy-manager.c b/src/sensor-proxy-manager.c new file mode 100644 index 000000000..b3ca6792e --- /dev/null +++ b/src/sensor-proxy-manager.c @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-sensor-proxy-manager" + +#include "phosh-config.h" +#include "sensor-proxy-manager.h" + +#define IIO_SENSOR_PROXY_DBUS_NAME "net.hadess.SensorProxy" +#define IIO_SENSOR_PROXY_DBUS_IFACE_NAME "net.hadess.SensorProxy" +#define IIO_SENSOR_PROXY_DBUS_OBJECT "/net/hadess/SensorProxy" + +/** + * PhoshSensorProxyManager: + * + * Interface with iio-sensor-proxy + * + * The #PhoshSensorProxyManager is responsible for + * getting events from iio-sensor-proxy. + * + * This is just a minimal wrapper so we don't have to provide the + * object path, names and bus names in several places. + */ + + +typedef struct _PhoshSensorProxyManager +{ + PhoshDBusSensorProxyProxy parent; + +} PhoshSensorProxyManager; + + +G_DEFINE_TYPE (PhoshSensorProxyManager, phosh_sensor_proxy_manager, + PHOSH_DBUS_TYPE_SENSOR_PROXY_PROXY) + + +static void +phosh_sensor_proxy_manager_class_init (PhoshSensorProxyManagerClass *klass) +{ +} + + +static void +phosh_sensor_proxy_manager_init (PhoshSensorProxyManager *self) +{ +} + + +PhoshSensorProxyManager * +phosh_sensor_proxy_manager_new (GError **err) +{ + return g_initable_new (PHOSH_TYPE_SENSOR_PROXY_MANAGER, NULL, err, + "g-flags", G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + "g-name", IIO_SENSOR_PROXY_DBUS_NAME, + "g-bus-type", G_BUS_TYPE_SYSTEM, + "g-object-path", IIO_SENSOR_PROXY_DBUS_OBJECT, + "g-interface-name", IIO_SENSOR_PROXY_DBUS_IFACE_NAME, + NULL); +} diff --git a/src/sensor-proxy-manager.h b/src/sensor-proxy-manager.h new file mode 100644 index 000000000..32664a6d4 --- /dev/null +++ b/src/sensor-proxy-manager.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "dbus/iio-sensor-proxy-dbus.h" + +#define PHOSH_TYPE_SENSOR_PROXY_MANAGER (phosh_sensor_proxy_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshSensorProxyManager, phosh_sensor_proxy_manager, + PHOSH, SENSOR_PROXY_MANAGER, PhoshDBusSensorProxyProxy) + +PhoshSensorProxyManager *phosh_sensor_proxy_manager_new (GError **err); +gboolean phosh_sensor_proxy_manager_claim_proximity_sync (PhoshSensorProxyManager *self, + GError **err); diff --git a/src/session-manager.c b/src/session-manager.c new file mode 100644 index 000000000..d1c2ae1c7 --- /dev/null +++ b/src/session-manager.c @@ -0,0 +1,547 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-session-manager" + +#include "phosh-config.h" +#include "end-session-dialog.h" +#include "session-manager.h" +#include "shell-priv.h" +#include "util.h" + +#include "dbus/gnome-session-dbus.h" +#include "dbus/gnome-session-client-private-dbus.h" + +#define BUS_NAME "org.gnome.SessionManager" +#define OBJECT_PATH "/org/gnome/SessionManager" + +#define END_SESSION_DIALOG_OBJECT_PATH "/org/gnome/SessionManager/EndSessionDialog" + +#define SESSION_SHUTDOWN_TIMEOUT 15 + +/** + * PhoshSessionManager: + * + * Session interaction + * + * The #PhoshSessionManager is responsible for + * managing attributes of the session. + */ + +enum { + PHOSH_SESSION_MANAGER_PROP_0, + PHOSH_SESSION_MANAGER_PROP_ACTIVE, + PHOSH_SESSION_MANAGER_PROP_LAST_PROP, +}; +static GParamSpec *props[PHOSH_SESSION_MANAGER_PROP_LAST_PROP]; + +static void phosh_session_manager_end_session_dialog_iface_init ( + PhoshDBusEndSessionDialogIface *iface); + +typedef struct _PhoshSessionManager { + PhoshDBusEndSessionDialogSkeleton parent; + gboolean active; + + PhoshDBusSessionManager *proxy; + GCancellable *cancel; + PhoshDBusSessionManagerClientPrivate *priv_proxy; + + PhoshEndSessionDialog *dialog; +} PhoshSessionManager; + + +G_DEFINE_TYPE_WITH_CODE (PhoshSessionManager, + phosh_session_manager, + PHOSH_DBUS_TYPE_END_SESSION_DIALOG_SKELETON, + G_IMPLEMENT_INTERFACE ( + PHOSH_DBUS_TYPE_END_SESSION_DIALOG, + phosh_session_manager_end_session_dialog_iface_init)) + + +static void +on_end_session_dialog_closed (PhoshSessionManager *self, PhoshEndSessionDialog *dialog) +{ + gint action; + gboolean confirmed; + PhoshDBusEndSessionDialog *object; + + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_IS_END_SESSION_DIALOG (dialog)); + + object = PHOSH_DBUS_END_SESSION_DIALOG (self); + + confirmed = phosh_end_session_dialog_get_action_confirmed (dialog); + action = phosh_end_session_dialog_get_action (dialog); + g_clear_pointer ((PhoshSystemModalDialog**)&self->dialog, phosh_system_modal_dialog_close); + + g_debug ("Action %d confirmed: %d", action, confirmed); + + if (!confirmed) { + phosh_dbus_end_session_dialog_emit_canceled (object); + return; + } + + switch (action) { + case PHOSH_END_SESSION_ACTION_LOGOUT: + phosh_dbus_end_session_dialog_emit_confirmed_logout (object); + break; + case PHOSH_END_SESSION_ACTION_SHUTDOWN: + phosh_dbus_end_session_dialog_emit_confirmed_shutdown (object); + break; + case PHOSH_END_SESSION_ACTION_REBOOT: + phosh_dbus_end_session_dialog_emit_confirmed_reboot (object); + break; + /* not used by gnome-session */ + case PHOSH_END_SESSION_ACTION_HIBERNATE: + case PHOSH_END_SESSION_ACTION_SUSPEND: + case PHOSH_END_SESSION_ACTION_HYBRID_SLEEP: + default: + g_return_if_reached (); + } +} + + +static gboolean +handle_end_session_open (PhoshDBusEndSessionDialog *object, + GDBusMethodInvocation *invocation, + guint arg_type, + guint arg_timestamp, + guint arg_seconds_to_stay_open, + const char *const *arg_inhibitor_object_paths) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (object); + + g_debug ("DBus call %s, type: %d, seconds %d", + __func__, arg_type, arg_seconds_to_stay_open); + + if (self->dialog != NULL) { + g_object_set (self->dialog, + "inhibitor-paths", arg_inhibitor_object_paths, + NULL); + gtk_widget_set_visible (GTK_WIDGET (self->dialog), TRUE); + phosh_dbus_end_session_dialog_complete_open ( + object, invocation); + return TRUE; + } + + self->dialog = PHOSH_END_SESSION_DIALOG (phosh_end_session_dialog_new (arg_type, + arg_seconds_to_stay_open, + arg_inhibitor_object_paths)); + g_signal_connect_swapped (self->dialog, "closed", + G_CALLBACK (on_end_session_dialog_closed), self); + + gtk_widget_set_visible (GTK_WIDGET (self->dialog), TRUE); + phosh_dbus_end_session_dialog_complete_open (object, invocation); + + return TRUE; +} + + +static void +phosh_session_manager_end_session_dialog_iface_init (PhoshDBusEndSessionDialogIface *iface) +{ + iface->handle_open = handle_end_session_open; +} + + +static void +phosh_session_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (object); + + switch (property_id) { + case PHOSH_SESSION_MANAGER_PROP_ACTIVE: + g_value_set_boolean (value, self->active); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_session_active_changed (PhoshSessionManager *self, + GParamSpec *pspec, + PhoshDBusSessionManager *proxy) +{ + gboolean active; + + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_SESSION_MANAGER_PROXY (proxy)); + + active = phosh_dbus_session_manager_get_session_is_active (proxy); + + if (self->active == active) + return; + + self->active = active; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_SESSION_MANAGER_PROP_ACTIVE]); + g_debug ("Session is now %sactive", self->active ? "" : "in"); +} + + +static void +on_end_session_response_finish (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + + if (!phosh_dbus_session_manager_client_private_call_end_session_response_finish ( + self->priv_proxy, res, &err)) { + g_warning ("Failed end session response: %s", err->message); + goto out; + } +out: + g_object_unref (self); +} + + +static void +respond_to_end_session (PhoshSessionManager *self, gboolean shutdown) +{ + if (shutdown) + phosh_shell_fade_out (phosh_shell_get_default (), SESSION_SHUTDOWN_TIMEOUT); + + phosh_dbus_session_manager_client_private_call_end_session_response ( + self->priv_proxy, + TRUE, + "", + NULL, + on_end_session_response_finish, + g_object_ref (self)); +} + + +static void +on_query_end_session (PhoshSessionManager *self, + guint flags, + PhoshDBusSessionManagerClientPrivate *object) +{ + respond_to_end_session (self, FALSE); +} + + +static void +on_end_session (PhoshSessionManager *self, + guint flags, + PhoshDBusSessionManagerClientPrivate *object) +{ + respond_to_end_session (self, TRUE); +} + + +static void +on_stop (PhoshSessionManager *self, PhoshDBusSessionManagerClientPrivate *object) +{ + gtk_main_quit (); +} + + +static void +on_client_private_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + + self->priv_proxy = phosh_dbus_session_manager_client_private_proxy_new_for_bus_finish ( + res, &err); + if (!self->priv_proxy) { + g_warning ("Failed to get client private proxy: %s", err->message); + goto out; + } + + g_debug ("Private client initialized"); + + g_object_connect (self->priv_proxy, + "swapped-signal::query-end-session", on_query_end_session, self, + "swapped-signal::end-session", on_end_session, self, + "swapped-signal::stop", on_stop, self, + NULL); +out: + g_object_unref (self); +} + + +static void +on_client_registered (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSessionManager *proxy = PHOSH_DBUS_SESSION_MANAGER (source_object); + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autofree char *client_id = NULL; + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_session_manager_call_register_client_finish (proxy, &client_id, res, &err)) { + phosh_async_error_warn (err, "Failed to register client"); + return; + } + g_debug ("Registered client at '%s'", client_id); + + phosh_dbus_session_manager_client_private_proxy_new_for_bus ( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + BUS_NAME, + client_id, + NULL, + on_client_private_proxy_new_for_bus_finish, + g_object_ref (self)); +} + + +static void +on_logout_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSessionManager *proxy = PHOSH_DBUS_SESSION_MANAGER (source_object); + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_session_manager_call_logout_finish (proxy, res, &err)) + g_warning ("Failed to logout: %s", err->message); + g_object_unref (self); +} + + +static void +on_shutdown_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSessionManager *proxy = PHOSH_DBUS_SESSION_MANAGER (source_object); + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_session_manager_call_shutdown_finish (proxy, res, &err)) + g_warning ("Failed to shutdown: %s", err->message); + g_object_unref (self); +} + + +static void +on_reboot_finished (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusSessionManager *proxy = PHOSH_DBUS_SESSION_MANAGER (source_object); + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_session_manager_call_reboot_finish (proxy, res, &err)) + g_warning ("Failed to reboot: %s", err->message); + g_object_unref (self); +} + + +static void +phosh_session_manager_constructed (GObject *object) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (object); + + g_autoptr (GError) err = NULL; + + /* Sync call since this happens early in startup and we need it right away */ + self->proxy = phosh_dbus_session_manager_proxy_new_for_bus_sync ( + G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START_AT_CONSTRUCTION, + BUS_NAME, + OBJECT_PATH, + NULL, + &err); + + if (!self->proxy) { + g_warning ("Failed to get session proxy %s", err->message); + } else { + /* Don't use a property binding so 'active' can be a r/o property */ + g_signal_connect_swapped (self->proxy, + "notify::session-is-active", + G_CALLBACK (on_session_active_changed), + self); + on_session_active_changed (self, NULL, self->proxy); + } + + G_OBJECT_CLASS (phosh_session_manager_parent_class)->constructed (object); +} + + +static void +phosh_session_manager_dispose (GObject *object) +{ + PhoshSessionManager *self = PHOSH_SESSION_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + if (g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self))) + g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self)); + + g_clear_pointer (&self->dialog, phosh_cp_widget_destroy); + g_clear_object (&self->priv_proxy); + g_clear_object (&self->proxy); + + G_OBJECT_CLASS (phosh_session_manager_parent_class)->dispose (object); +} + + +static void +phosh_session_manager_class_init (PhoshSessionManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_session_manager_constructed; + object_class->dispose = phosh_session_manager_dispose; + object_class->get_property = phosh_session_manager_get_property; + + /** + * PhoshSessionManager:active: + * + * Whether this phosh instance runs in the currently active session. + */ + props[PHOSH_SESSION_MANAGER_PROP_ACTIVE] = + g_param_spec_boolean ("active", + "Active", + "Active session", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PHOSH_SESSION_MANAGER_PROP_LAST_PROP, props); +} + + +static void +phosh_session_manager_init (PhoshSessionManager *self) +{ + self->cancel = g_cancellable_new (); +} + + +PhoshSessionManager * +phosh_session_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_SESSION_MANAGER, NULL); +} + + +gboolean +phosh_session_manager_is_active (PhoshSessionManager *self) +{ + g_return_val_if_fail (PHOSH_IS_SESSION_MANAGER (self), FALSE); + + return self->active; +} + +void +phosh_session_manager_register (PhoshSessionManager *self, + const char *app_id, + const char *startup_id) +{ + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_SESSION_MANAGER_PROXY (self->proxy)); + g_return_if_fail (app_id != NULL); + + phosh_dbus_session_manager_call_register_client (self->proxy, + app_id, + startup_id ? startup_id : "", + self->cancel, + on_client_registered, + self); +} + +void +phosh_session_manager_logout (PhoshSessionManager *self) +{ + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_SESSION_MANAGER_PROXY (self->proxy)); + + phosh_dbus_session_manager_call_logout (self->proxy, + 1 /* no dialog */, + NULL, + on_logout_finished, + g_object_ref (self)); +} + + +void +phosh_session_manager_shutdown (PhoshSessionManager *self) +{ + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_SESSION_MANAGER_PROXY (self->proxy)); + + phosh_dbus_session_manager_call_shutdown (self->proxy, + NULL, + on_shutdown_finished, + g_object_ref (self)); +} + + +void +phosh_session_manager_reboot (PhoshSessionManager *self) +{ + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_SESSION_MANAGER_PROXY (self->proxy)); + + phosh_dbus_session_manager_call_reboot (self->proxy, + NULL, + on_reboot_finished, + g_object_ref (self)); +} + +void +phosh_session_manager_export_end_session (PhoshSessionManager *self, + GDBusConnection *connection) +{ + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (self)); + + g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self), + connection, + END_SESSION_DIALOG_OBJECT_PATH, + NULL); +} + +guint +phosh_session_manager_inhibit (PhoshSessionManager *self, + PhoshSessionManagerFlags what, + const char *reason) +{ + g_autoptr (GError) err = NULL; + gboolean success; + guint cookie; + + success = phosh_dbus_session_manager_call_inhibit_sync (self->proxy, + PHOSH_APP_ID, + 0, + reason, + what, + &cookie, + self->cancel, + &err); + if (!success) { + g_warning ("Failed to inhibit %d: %s", what, err->message); + return 0; + } + + return cookie; +} + + +void +phosh_session_manager_uninhibit (PhoshSessionManager *self, guint cookie) +{ + g_autoptr (GError) err = NULL; + gboolean success; + + success = phosh_dbus_session_manager_call_uninhibit_sync (self->proxy, + cookie, + self->cancel, + &err); + if (!success) + g_warning ("Failed to uninhibit %u: %s", cookie, err->message); +} diff --git a/src/session-manager.h b/src/session-manager.h new file mode 100644 index 000000000..9468b64f0 --- /dev/null +++ b/src/session-manager.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "dbus/phosh-end-session-dialog-dbus.h" + +#include +#include + +G_BEGIN_DECLS + +typedef enum _PhoshSessionInhibitFlags { + PHOSH_SESSION_INHIBIT_LOGOUT = (1 << 0), + PHOSH_SESSION_INHIBIT_USER_SWITCH = (1 << 1), + PHOSH_SESSION_INHIBIT_SUSPEND = (1 << 2), + PHOSH_SESSION_INHIBIT_IDLE = (1 << 3), + PHOSH_SESSION_INHIBIT_AUTOMOUNT = (1 << 4), +} PhoshSessionManagerFlags; + +#define PHOSH_TYPE_SESSION_MANAGER phosh_session_manager_get_type () + +G_DECLARE_FINAL_TYPE (PhoshSessionManager, phosh_session_manager, + PHOSH, SESSION_MANAGER, PhoshDBusEndSessionDialogSkeleton) + +PhoshSessionManager *phosh_session_manager_new (void); +gboolean phosh_session_manager_is_active (PhoshSessionManager *self); +void phosh_session_manager_register (PhoshSessionManager *self, const char *app_id, const char *startup_id); +void phosh_session_manager_logout (PhoshSessionManager *self); +void phosh_session_manager_shutdown (PhoshSessionManager *self); +void phosh_session_manager_reboot (PhoshSessionManager *self); +guint phosh_session_manager_inhibit (PhoshSessionManager *self, + PhoshSessionManagerFlags what, + const char *reason); +void phosh_session_manager_uninhibit (PhoshSessionManager *self, guint cookie); + +void phosh_session_manager_export_end_session (PhoshSessionManager *self, + GDBusConnection *connection); + +G_END_DECLS diff --git a/src/session-presence.c b/src/session-presence.c new file mode 100644 index 000000000..0b4da52c4 --- /dev/null +++ b/src/session-presence.c @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-session-presence" + +#include "phosh-config.h" +#include "session-presence.h" + +#define GNOME_SESSION_DBUS_NAME "org.gnome.SessionManager" +#define GNOME_SESSION_DBUS_OBJECT "/org/gnome/SessionManager/Presence" + +/** + * PhoshSessionPresence: + * + * Interface with gnome-session's Presence interface + * + * The #PhoshSessionPresence is responsible for getting status updated + * from gnome-session's org.gnome.SessionManager.Presence interface. + * + * This is just a minimal wrapper so we don't have to provide the + * object path, names and bus names in several places. + */ + +typedef struct _PhoshSessionPresence { + PhoshDBusSessionManagerPresenceProxy parent; +} PhoshSessionPresence; + + +G_DEFINE_TYPE (PhoshSessionPresence, phosh_session_presence, + PHOSH_DBUS_TYPE_SESSION_MANAGER_PRESENCE_PROXY) + + +static void +phosh_session_presence_class_init (PhoshSessionPresenceClass *klass) +{ +} + + +static void +phosh_session_presence_init (PhoshSessionPresence *self) +{ +} + +/** + * phosh_session_presence_get_default_failable: + * + * Get the session presence singleton + * + * Returns:(transfer none): The session presence singleton + */ +PhoshSessionPresence * +phosh_session_presence_get_default_failable (void) +{ + static PhoshSessionPresence *instance; + GError *err = NULL; + GInitable *ret; + + if (instance == NULL) { + ret = g_initable_new (PHOSH_TYPE_SESSION_PRESENCE, NULL, &err, + "g-flags", G_DBUS_OBJECT_MANAGER_CLIENT_FLAGS_NONE, + "g-name", GNOME_SESSION_DBUS_NAME, + "g-bus-type", G_BUS_TYPE_SESSION, + "g-object-path", GNOME_SESSION_DBUS_OBJECT, + "g-interface-name", "org.gnome.SessionManager.Presence", + NULL); + if (ret != NULL) { + instance = PHOSH_SESSION_PRESENCE (ret); + } else { + g_warning ("Can't connect to session at %s: %s", GNOME_SESSION_DBUS_OBJECT, err->message); + return NULL; + } + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} diff --git a/src/session-presence.h b/src/session-presence.h new file mode 100644 index 000000000..0b138335e --- /dev/null +++ b/src/session-presence.h @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "dbus/gnome-session-presence-dbus.h" + +G_BEGIN_DECLS + +/** + * PhoshSessionPresenceStatus: + * + * Current (GNOME) session's presence status These need to match the + * values of the DBus protocol: + * https://people.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Presence:status + */ +typedef enum { + /**/ + PHOSH_SESSION_PRESENCE_STATUS_AVAILABLE = 0, + PHOSH_SESSION_PRESENCE_STATUS_INVISIBLE, + PHOSH_SESSION_PRESENCE_STATUS_BUSY, + PHOSH_SESSION_PRESENCE_STATUS_IDLE, +} PhoshSessionPresenceStatus; + +#define PHOSH_TYPE_SESSION_PRESENCE (phosh_session_presence_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshSessionPresence, phosh_session_presence, PHOSH, SESSION_PRESENCE, + PhoshDBusSessionManagerPresenceProxy) + +PhoshSessionPresence *phosh_session_presence_get_default_failable (void); + +G_END_DECLS diff --git a/src/settings.c b/src/settings.c new file mode 100644 index 000000000..7c856fd6a --- /dev/null +++ b/src/settings.c @@ -0,0 +1,516 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-settings" + +#include + +#include "phosh-config.h" + +#include "brightness-settings.h" +#include "media-player.h" +#include "shell-priv.h" +#include "settings.h" +#include "quick-settings.h" +#include "settings/audio-settings.h" +#include "torch-manager.h" +#include "notifications/notify-manager.h" +#include "notifications/notification-frame.h" + +#include +#include + +#define STACK_CHILD_NOTIFICATIONS "notifications" +#define STACK_CHILD_NO_NOTIFICATIONS "no-notifications" + +/** + * PhoshSettings: + * + * The settings menu + */ +enum { + PROP_0, + PROP_ON_LOCKSCREEN, + PROP_DRAG_HANDLE_OFFSET, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + SETTING_DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct _PhoshSettings { + GtkBin parent; + + gboolean on_lockscreen; + gint drag_handle_offset; + guint debounce_handle; + + GtkWidget *scrolled_window; + GtkWidget *box_sliders; + GtkWidget *box_settings; + GtkWidget *quick_settings; + GtkWidget *media_player; + PhoshAudioSettings *audio_settings; + PhoshBrightnessSettings *brightness_settings; + + /* The area with media widget, notifications */ + GtkWidget *box_bottom_half; + /* Notifications */ + GtkWidget *list_notifications; + GtkWidget *stack_notifications; + + /* Torch */ + PhoshTorchManager *torch_manager; + GtkWidget *revealer; + GtkWidget *scale_torch; + gboolean setting_torch; +} PhoshSettings; + + +G_DEFINE_TYPE (PhoshSettings, phosh_settings, GTK_TYPE_BIN) + + +static void +set_on_lockscreen (PhoshSettings *self, gboolean on_lockscreen) +{ + if (self->on_lockscreen == on_lockscreen) + return; + + self->on_lockscreen = on_lockscreen; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ON_LOCKSCREEN]); +} + + +static void +phosh_settings_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSettings *self = PHOSH_SETTINGS (object); + + switch (property_id) { + case PROP_ON_LOCKSCREEN: + set_on_lockscreen (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_settings_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSettings *self = PHOSH_SETTINGS (object); + + switch (property_id) { + case PROP_ON_LOCKSCREEN: + g_value_set_boolean (value, self->on_lockscreen); + break; + case PROP_DRAG_HANDLE_OFFSET: + g_value_set_int (value, self->drag_handle_offset); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +calc_drag_handle_offset (PhoshSettings *self) +{ + int h = 0; + int box_height, sw_height, qs_height = 0; + int qs_y = 0; + gboolean success; + + h = gtk_widget_get_allocated_height (GTK_WIDGET (self)); + /* On the lock screen the whole surface is fine */ + if (self->on_lockscreen) + goto out; + + box_height = gtk_widget_get_allocated_height (self->box_settings); + sw_height = gtk_widget_get_allocated_height (self->scrolled_window); + if (box_height > sw_height) { + h = 0; /* Don't enlarge drag handle if box needs scrolling */ + goto out; + } + + g_debug ("Calculating drag offset: on quick-settings"); + + success = gtk_widget_translate_coordinates (self->quick_settings, GTK_WIDGET (self), + 0, 0, NULL, &qs_y); + + if (!success) { + g_warning ("Calculating drag offset: Unable to get quick-setting's y coordinate"); + goto out; + } + + qs_height = gtk_widget_get_allocated_height (self->quick_settings); + h = qs_y + qs_height; + + g_debug ("Calculating drag offset: QS_y = %d, QS_height = %d, height = %d", + qs_y, qs_height, h); + + out: + if (self->drag_handle_offset == h) + return; + + self->drag_handle_offset = h; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAG_HANDLE_OFFSET]); +} + + +static void +on_size_allocate (PhoshSettings *self) +{ + calc_drag_handle_offset (self); + + return; +} + + +static void +delayed_update_drag_handle_offset (gpointer data) +{ + PhoshSettings *self = PHOSH_SETTINGS (data); + + self->debounce_handle = 0; + calc_drag_handle_offset (self); +} + + +static void +update_drag_handle_offset (PhoshSettings *self) +{ + g_clear_handle_id (&self->debounce_handle, g_source_remove); + self->debounce_handle = g_timeout_add_once (200, delayed_update_drag_handle_offset, self); + g_source_set_name_by_id (self->debounce_handle, "[phosh] delayed_update_drag_handle_offset"); +} + + +static void +on_is_headphone_changed (PhoshSettings *self, + GParamSpec *pspec, + PhoshAudioSettings *audio_settings) +{ + PhoshMediaPlayer *media_player; + + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + g_return_if_fail (PHOSH_IS_AUDIO_SETTINGS (audio_settings)); + media_player = PHOSH_MEDIA_PLAYER (self->media_player); + + if (phosh_audio_settings_get_output_is_headphone (self->audio_settings) || + !phosh_media_player_get_is_playable (media_player) || + phosh_media_player_get_status (media_player) != PHOSH_MEDIA_PLAYER_STATUS_PLAYING) + return; + + phosh_media_player_toggle_play_pause (media_player); +} + + +static void +on_media_player_raised (PhoshSettings *self, + gpointer unused) +{ + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + g_signal_emit (self, signals[SETTING_DONE], 0); +} + + +static void +on_notifications_clear_all_clicked (PhoshSettings *self) +{ + PhoshNotifyManager *manager; + + manager = phosh_notify_manager_get_default (); + phosh_notify_manager_close_all_notifications (manager, PHOSH_NOTIFICATION_REASON_DISMISSED); +} + + +static GtkWidget * +create_notification_row (gpointer item, gpointer data) +{ + PhoshShell *shell = phosh_shell_get_default (); + GtkWidget *row = NULL; + GtkWidget *frame = NULL; + + row = g_object_new (GTK_TYPE_LIST_BOX_ROW, + "activatable", FALSE, + "visible", TRUE, + NULL); + + frame = phosh_notification_frame_new (TRUE, NULL); + phosh_notification_frame_bind_model (PHOSH_NOTIFICATION_FRAME (frame), item); + + if (!(phosh_shell_get_state (shell) & PHOSH_STATE_SETTINGS)) + phosh_notification_frame_set_animate_show (PHOSH_NOTIFICATION_FRAME (frame), FALSE); + gtk_widget_set_visible (frame, TRUE); + + gtk_container_add (GTK_CONTAINER (row), frame); + + return row; +} + + +static void +on_torch_scale_value_changed (PhoshSettings *self, GtkScale *scale_torch) +{ + double value; + + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (self->torch_manager)); + + /* Only react to scale changes when torch is enabled */ + if (!phosh_torch_manager_get_enabled (self->torch_manager)) + return; + + self->setting_torch = TRUE; + value = gtk_range_get_value (GTK_RANGE (self->scale_torch)); + g_debug ("Setting torch brightness to %.2f", value); + phosh_torch_manager_set_scaled_brightness (self->torch_manager, value / 100.0); +} + + +static void +on_torch_brightness_changed (PhoshSettings *self, GParamSpec *pspec, PhoshTorchManager *manager) +{ + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (manager)); + + if (self->setting_torch) { + self->setting_torch = FALSE; + return; + } + + gtk_range_set_value (GTK_RANGE (self->scale_torch), + 100.0 * phosh_torch_manager_get_scaled_brightness (self->torch_manager)); +} + + +static void +on_notification_frames_items_changed (PhoshSettings *self, + guint position, + guint removed, + guint added, + GListModel *list) +{ + gboolean is_empty; + const char *child_name; + + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + g_return_if_fail (G_IS_LIST_MODEL (list)); + + is_empty = !g_list_model_get_n_items (list); + g_debug ("Notification list empty: %d", is_empty); + + child_name = is_empty ? STACK_CHILD_NO_NOTIFICATIONS : STACK_CHILD_NOTIFICATIONS; + gtk_stack_set_visible_child_name (GTK_STACK (self->stack_notifications), child_name); + update_drag_handle_offset (self); +} + + +static void +setup_torch (PhoshSettings *self) +{ + PhoshShell *shell = phosh_shell_get_default (); + + self->torch_manager = g_object_ref (phosh_shell_get_torch_manager (shell)); + + g_object_bind_property (self->torch_manager, "enabled", + self->revealer, "reveal-child", + G_BINDING_SYNC_CREATE); + gtk_range_set_range (GTK_RANGE (self->scale_torch), 0, 100); + gtk_range_set_value (GTK_RANGE (self->scale_torch), + phosh_torch_manager_get_scaled_brightness (self->torch_manager) * 100.0); + g_signal_connect_object (self->torch_manager, + "notify::brightness", + G_CALLBACK (on_torch_brightness_changed), + self, + G_CONNECT_SWAPPED); + g_object_bind_property (self->torch_manager, "can-scale", + self->revealer, "visible", + G_BINDING_SYNC_CREATE); +} + + +static void +phosh_settings_constructed (GObject *object) +{ + PhoshSettings *self = PHOSH_SETTINGS (object); + PhoshNotifyManager *manager; + + G_OBJECT_CLASS (phosh_settings_parent_class)->constructed (object); + + setup_torch (self); + + manager = phosh_notify_manager_get_default (); + gtk_list_box_bind_model (GTK_LIST_BOX (self->list_notifications), + G_LIST_MODEL (phosh_notify_manager_get_list (manager)), + create_notification_row, + NULL, + NULL); + g_signal_connect_object (phosh_notify_manager_get_list (manager), + "items-changed", + G_CALLBACK (on_notification_frames_items_changed), + self, + G_CONNECT_SWAPPED); + on_notification_frames_items_changed (self, -1, -1, -1, + G_LIST_MODEL (phosh_notify_manager_get_list (manager))); + + g_object_bind_property (phosh_shell_get_default (), + "locked", + self, + "on-lockscreen", + G_BINDING_SYNC_CREATE); +} + + +static void +phosh_settings_dispose (GObject *object) +{ + PhoshSettings *self = PHOSH_SETTINGS (object); + + g_clear_object (&self->torch_manager); + + G_OBJECT_CLASS (phosh_settings_parent_class)->dispose (object); +} + + +static void +phosh_settings_finalize (GObject *object) +{ + PhoshSettings *self = PHOSH_SETTINGS (object); + + g_clear_handle_id (&self->debounce_handle, g_source_remove); + + G_OBJECT_CLASS (phosh_settings_parent_class)->finalize (object); +} + + +static void +phosh_settings_class_init (PhoshSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->dispose = phosh_settings_dispose; + object_class->finalize = phosh_settings_finalize; + object_class->constructed = phosh_settings_constructed; + object_class->set_property = phosh_settings_set_property; + object_class->get_property = phosh_settings_get_property; + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/settings.ui"); + + /* PhoshSettings:on-lockscreen: + * + * Whether settings are shown on lockscreen (%TRUE) or in the unlocked shell + * (%FALSE). + * + * Consider this property to be read only. It's only r/w so we can + * use a property binding with the [type@Shell]s "locked" property. + */ + props[PROP_ON_LOCKSCREEN] = + g_param_spec_boolean ( + "on-lockscreen", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + /* PhoshSettings:handle-offset: + * + * The offset from the bottom of the widget where it's safe to start + * dragging. See phosh_settings_get_drag_drag_handle_offset(). + */ + props[PROP_DRAG_HANDLE_OFFSET] = + g_param_spec_int ( + "drag-handle-offset", "", "", + 0, + G_MAXINT, + 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + g_type_ensure (PHOSH_TYPE_AUDIO_SETTINGS); + g_type_ensure (PHOSH_TYPE_BRIGHTNESS_SETTINGS); + g_type_ensure (PHOSH_TYPE_QUICK_SETTINGS); + + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, audio_settings); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, brightness_settings); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, box_bottom_half); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, box_sliders); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, box_settings); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, list_notifications); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, media_player); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, quick_settings); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, revealer); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, scale_torch); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, stack_notifications); + gtk_widget_class_bind_template_child (widget_class, PhoshSettings, scrolled_window); + + gtk_widget_class_bind_template_callback (widget_class, on_media_player_raised); + gtk_widget_class_bind_template_callback (widget_class, on_is_headphone_changed); + gtk_widget_class_bind_template_callback (widget_class, on_notifications_clear_all_clicked); + gtk_widget_class_bind_template_callback (widget_class, on_torch_scale_value_changed); + gtk_widget_class_bind_template_callback (widget_class, update_drag_handle_offset); +} + + +static void +phosh_settings_init (PhoshSettings *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + g_signal_connect (self, "size-allocate", G_CALLBACK (on_size_allocate), NULL); +} + + +GtkWidget * +phosh_settings_new (void) +{ + return g_object_new (PHOSH_TYPE_SETTINGS, NULL); +} + +/** + * phosh_settings_get_drag_handle_offset: + * @self: The settings + * + * Get the y coordinate from the top of the widget where dragging + * can start. E.g. we don't want drag to work on notifications as + * notifications need to scroll in vertical direction. + * + * Returns: The y coordinate at which dragging the surface can start. + */ +gint +phosh_settings_get_drag_handle_offset (PhoshSettings *self) +{ + g_return_val_if_fail (PHOSH_IS_SETTINGS (self), 0); + + return self->drag_handle_offset; +} + + +void +phosh_settings_hide_details (PhoshSettings *self) +{ + g_return_if_fail (PHOSH_IS_SETTINGS (self)); + + phosh_audio_settings_hide_details (self->audio_settings); + phosh_brightness_settings_hide_details (self->brightness_settings); + phosh_quick_settings_hide_status (PHOSH_QUICK_SETTINGS (self->quick_settings)); +} diff --git a/src/settings.h b/src/settings.h new file mode 100644 index 000000000..8c6ede28e --- /dev/null +++ b/src/settings.h @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "wifi-status-page.h" + +#define PHOSH_TYPE_SETTINGS (phosh_settings_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshSettings, phosh_settings, PHOSH, SETTINGS, GtkBin) + +GtkWidget * phosh_settings_new (void); +gint phosh_settings_get_drag_handle_offset (PhoshSettings *self); +void phosh_settings_hide_details (PhoshSettings *self); diff --git a/src/settings/audio-device-row.c b/src/settings/audio-device-row.c new file mode 100644 index 000000000..c3b7fa887 --- /dev/null +++ b/src/settings/audio-device-row.c @@ -0,0 +1,184 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-audio_device-row" + +#include "phosh-config.h" + +#include "audio/audio-device.h" +#include "audio-device-row.h" + +/** + * PhoshAudioDeviceRow: + * + * A widget intended to be stored in a `GtkListBox` to represent and audio device. + */ + +enum { + PROP_0, + PROP_AUDIO_DEVICE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshAudioDeviceRow { + GtkListBoxRow parent; + + PhoshAudioDevice *audio_device; + + GtkWidget *icon; + GtkWidget *description; + GtkWidget *revealer; +}; +G_DEFINE_TYPE (PhoshAudioDeviceRow, phosh_audio_device_row, GTK_TYPE_LIST_BOX_ROW) + + +static gboolean +transform_icon_name_to_icon (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer unused) +{ + const char *icon_name = g_value_get_string (from_value); + GIcon *icon = NULL; + + if (icon_name == NULL) + icon_name = "audio-speakers-symbolic"; + + icon = g_themed_icon_new_with_default_fallbacks (icon_name); + + g_value_take_object (to_value, icon); + return TRUE; +} + + +static void +set_audio_device (PhoshAudioDeviceRow *self, PhoshAudioDevice *device) +{ + g_set_object (&self->audio_device, device); + + g_object_bind_property (device, "description", + self->description, "label", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + g_object_bind_property_full (device, "icon-name", + self->icon, "gicon", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_icon_name_to_icon, + NULL, NULL, NULL); + + g_object_bind_property (device, "active", + self->revealer, "reveal-child", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +} + + +static void +phosh_audio_device_row_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDeviceRow *self = PHOSH_AUDIO_DEVICE_ROW (object); + + switch (property_id) { + case PROP_AUDIO_DEVICE: + set_audio_device (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_device_row_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAudioDeviceRow *self = PHOSH_AUDIO_DEVICE_ROW (object); + + switch (property_id) { + case PROP_AUDIO_DEVICE: + g_value_set_object (value, self->audio_device); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_device_row_dispose (GObject *object) +{ + PhoshAudioDeviceRow *self = PHOSH_AUDIO_DEVICE_ROW(object); + + g_clear_object (&self->audio_device); + + G_OBJECT_CLASS (phosh_audio_device_row_parent_class)->dispose (object); +} + + +static void +phosh_audio_device_row_class_init (PhoshAudioDeviceRowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_audio_device_row_get_property; + object_class->set_property = phosh_audio_device_row_set_property; + object_class->dispose = phosh_audio_device_row_dispose; + + props[PROP_AUDIO_DEVICE] = + g_param_spec_object ("audio-device", "", "", + PHOSH_TYPE_AUDIO_DEVICE, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/audio-device-row.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioDeviceRow, description); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioDeviceRow, icon); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioDeviceRow, revealer); +} + + +static void +phosh_audio_device_row_init (PhoshAudioDeviceRow *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshAudioDeviceRow * +phosh_audio_device_row_new (PhoshAudioDevice *audio_device) +{ + return g_object_new (PHOSH_TYPE_AUDIO_DEVICE_ROW, + "audio-device", audio_device, + NULL); +} + +/** + * phosh_audio_device_row_get_audio_device: + * @self: An audio device row + * + * Get the audio device associated with this row + * + * Returns:(transfer none): The audio device + */ +PhoshAudioDevice * +phosh_audio_device_row_get_audio_device (PhoshAudioDeviceRow *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_DEVICE_ROW (self), NULL); + + return self->audio_device; +} diff --git a/src/settings/audio-device-row.h b/src/settings/audio-device-row.h new file mode 100644 index 000000000..e8aa04ad3 --- /dev/null +++ b/src/settings/audio-device-row.h @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "audio/audio-device.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUDIO_DEVICE_ROW (phosh_audio_device_row_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAudioDeviceRow, phosh_audio_device_row, PHOSH, AUDIO_DEVICE_ROW, GtkListBoxRow) + +PhoshAudioDeviceRow *phosh_audio_device_row_new (PhoshAudioDevice *audio_device); +PhoshAudioDevice *phosh_audio_device_row_get_audio_device (PhoshAudioDeviceRow *self); + +G_END_DECLS diff --git a/src/settings/audio-settings.c b/src/settings/audio-settings.c new file mode 100644 index 000000000..37f7a898e --- /dev/null +++ b/src/settings/audio-settings.c @@ -0,0 +1,436 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-audio-settings" + +#include "phosh-config.h" + +#include + +#include "audio/audio-device.h" +#include "audio-manager.h" +#include "fading-label.h" +#include "settings/audio-device-row.h" +#include "settings/audio-settings.h" +#include "settings/channel-bar.h" +#include "util.h" + +#include "gvc-mixer-stream.h" + +#include + +#include + +#include + +/** + * PhoshAudioSettings: + * + * Widget to control Audio device selection and volume. + */ + +enum { + PROP_0, + PROP_IS_HEADPHONE, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshAudioSettings { + GtkBin parent; + + /* Volume slider */ + GvcMixerStream *output_stream; + gboolean allow_volume_above_100_percent; + gboolean setting_volume; + gboolean is_headphone; + PhoshChannelBar *output_vol_bar; + + /* Device select */ + GtkWidget *stack_audio_details; + GtkWidget *toggle_audio_details; + GtkWidget *box_audio_input_devices; + GtkWidget *box_audio_output_devices; + GtkWidget *listbox_audio_input_devices; + GtkWidget *listbox_audio_output_devices; + + PhoshAudioManager *audio_manager; +}; +G_DEFINE_TYPE (PhoshAudioSettings, phosh_audio_settings, GTK_TYPE_BIN) + + +static void +update_output_vol_bar (PhoshAudioSettings *self) +{ + GtkAdjustment *adj; + + self->setting_volume = TRUE; + phosh_channel_bar_set_base_volume (self->output_vol_bar, + gvc_mixer_stream_get_base_volume (self->output_stream)); + phosh_channel_bar_set_is_amplified (self->output_vol_bar, + self->allow_volume_above_100_percent && + gvc_mixer_stream_get_can_decibel (self->output_stream)); + adj = phosh_channel_bar_get_adjustment (self->output_vol_bar); + g_debug ("Adjusting volume to %d", gvc_mixer_stream_get_volume (self->output_stream)); + gtk_adjustment_set_value (adj, gvc_mixer_stream_get_volume (self->output_stream)); + self->setting_volume = FALSE; +} + + +static void +output_stream_notify_is_muted_cb (GvcMixerStream *stream, GParamSpec *pspec, gpointer data) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (data); + gboolean muted; + + muted = gvc_mixer_stream_get_is_muted (stream); + if (!self->setting_volume) { + phosh_channel_bar_set_is_muted (self->output_vol_bar, muted); + if (!muted) + update_output_vol_bar (self); + } +} + + +static void +output_stream_notify_volume_cb (GvcMixerStream *stream, GParamSpec *pspec, gpointer data) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (data); + + if (!self->setting_volume) + update_output_vol_bar (self); +} + + +static gboolean +stream_uses_headphones (GvcMixerStream *stream) +{ + const char *form_factor; + const GvcMixerStreamPort *port; + + form_factor = gvc_mixer_stream_get_form_factor (stream); + if (g_strcmp0 (form_factor, "headset") == 0 || + g_strcmp0 (form_factor, "headphone") == 0) { + return TRUE; + } + + port = gvc_mixer_stream_get_port (stream); + if (!port) + return FALSE; + + if (g_strcmp0 (port->port, "[Out] Headphones") == 0 || + g_strcmp0 (port->port, "analog-output-headphones") == 0) { + return TRUE; + } + + return FALSE; +} + + +static void +on_output_stream_port_changed (GvcMixerStream *stream, GParamSpec *pspec, gpointer data) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (data); + const char *icon = NULL; + gboolean is_headphone = FALSE; + const GvcMixerStreamPort *port; + GvcMixerControl *mixer_control; + + mixer_control = phosh_audio_manager_get_mixer_control (self->audio_manager); + port = gvc_mixer_stream_get_port (stream); + if (port) + g_debug ("Port changed: %s (%s)", port->human_port ?: port->port, port->port); + + is_headphone = stream_uses_headphones (stream); + if (is_headphone) { + icon = "audio-headphones"; + } else { + GvcMixerUIDevice *output; + + output = gvc_mixer_control_lookup_device_from_stream (mixer_control, stream); + if (output) + icon = gvc_mixer_ui_device_get_icon_name (output); + } + + if (gm_str_is_null_or_empty (icon) || g_str_has_prefix (icon, "audio-card")) + icon = "audio-speakers"; + + phosh_channel_bar_set_icon_name (self->output_vol_bar, icon); + + if (is_headphone == self->is_headphone) + return; + self->is_headphone = is_headphone; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_HEADPHONE]); +} + + +static void +mixer_control_output_update_cb (GvcMixerControl *mixer, guint id, gpointer data) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (data); + + g_debug ("Audio output updated: %d", id); + + g_return_if_fail (PHOSH_IS_AUDIO_SETTINGS (self)); + + if (self->output_stream) + g_signal_handlers_disconnect_by_data (self->output_stream, self); + + g_set_object (&self->output_stream, phosh_audio_manager_get_default_sink (self->audio_manager)); + g_return_if_fail (self->output_stream); + + g_signal_connect_object (self->output_stream, + "notify::volume", + G_CALLBACK (output_stream_notify_volume_cb), + self, 0); + + g_signal_connect_object (self->output_stream, + "notify::is-muted", + G_CALLBACK (output_stream_notify_is_muted_cb), + self, 0); + + g_signal_connect_object (self->output_stream, + "notify::port", + G_CALLBACK (on_output_stream_port_changed), + self, 0); + on_output_stream_port_changed (self->output_stream, NULL, self); + + update_output_vol_bar (self); +} + + +static void +vol_bar_value_changed_cb (PhoshChannelBar *bar, PhoshAudioSettings *self) +{ + double volume, rounded; + g_autofree char *name = NULL; + + if (!self->output_stream) + self->output_stream = g_object_ref (phosh_audio_manager_get_default_sink (self->audio_manager)); + + volume = phosh_channel_bar_get_volume (bar); + rounded = round (volume); + + g_object_get (self->output_vol_bar, "name", &name, NULL); + g_debug ("Setting stream volume %lf (rounded: %lf) for bar '%s'", volume, rounded, name); + + g_return_if_fail (self->output_stream); + if (gvc_mixer_stream_set_volume (self->output_stream, (pa_volume_t) rounded) != FALSE) + gvc_mixer_stream_push_volume (self->output_stream); + + gvc_mixer_stream_change_is_muted (self->output_stream, (int) rounded == 0); +} + + +static void +on_audio_input_device_row_activated (PhoshAudioSettings *self, + PhoshAudioDeviceRow *row, + GtkListBox *list) +{ + PhoshAudioDevice *audio_device = phosh_audio_device_row_get_audio_device (row); + guint id; + + g_return_if_fail (PHOSH_IS_AUDIO_DEVICE (audio_device)); + id = phosh_audio_device_get_id (audio_device); + + phosh_audio_manager_change_input (self->audio_manager, id); +} + + +static void +on_audio_output_device_row_activated (PhoshAudioSettings *self, + PhoshAudioDeviceRow *row, + GtkListBox *list) +{ + PhoshAudioDevice *audio_device = phosh_audio_device_row_get_audio_device (row); + guint id; + + g_return_if_fail (PHOSH_IS_AUDIO_DEVICE (audio_device)); + id = phosh_audio_device_get_id (audio_device); + + phosh_audio_manager_change_output (self->audio_manager, id); +} + + +static GtkWidget * +create_audio_device_row (gpointer item, gpointer user_data) +{ + PhoshAudioDevice *audio_device = PHOSH_AUDIO_DEVICE (item); + + return GTK_WIDGET (phosh_audio_device_row_new (audio_device)); +} + + +static void +phosh_audio_settings_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (object); + + switch (property_id) { + case PROP_IS_HEADPHONE: + g_value_set_boolean (value, phosh_audio_settings_get_output_is_headphone (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_audio_settings_dispose (GObject *object) +{ + PhoshAudioSettings *self = PHOSH_AUDIO_SETTINGS (object); + + g_clear_object (&self->output_stream); + + G_OBJECT_CLASS (phosh_audio_settings_parent_class)->dispose (object); +} + + +static void +phosh_audio_settings_class_init (PhoshAudioSettingsClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_audio_settings_get_property; + object_class->dispose = phosh_audio_settings_dispose; + + /** + * PhoshAudioSettings:is-headphone: + * + * Whether the current output is a headphone + */ + props[PROP_IS_HEADPHONE] = + g_param_spec_boolean ("is-headphone", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + g_type_ensure (PHOSH_TYPE_CHANNEL_BAR); + g_type_ensure (PHOSH_TYPE_FADING_LABEL); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/audio-settings.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, box_audio_input_devices); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, box_audio_output_devices); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, listbox_audio_input_devices); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, listbox_audio_output_devices); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, output_vol_bar); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, stack_audio_details); + gtk_widget_class_bind_template_child (widget_class, PhoshAudioSettings, toggle_audio_details); + + gtk_widget_class_bind_template_callback (widget_class, on_audio_input_device_row_activated); + gtk_widget_class_bind_template_callback (widget_class, on_audio_output_device_row_activated); + + gtk_widget_class_set_css_name (widget_class, "phosh-audio-settings"); +} + + +static gboolean +transform_toggle_stack_child_name (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer user_data) +{ + gboolean active = g_value_get_boolean (from_value); + + g_value_set_string (to_value, active ? "audio-details" : "no-audio-details"); + + return TRUE; +} + + +static void +phosh_audio_settings_init (PhoshAudioSettings *self) +{ + PhoshAudioDevices *input_devices, *output_devices; + GvcMixerControl *mixer_control; + + self->audio_manager = g_object_ref (phosh_audio_manager_get_default ()); + + gtk_widget_init_template (GTK_WIDGET (self)); + + mixer_control = phosh_audio_manager_get_mixer_control (self->audio_manager); + if (mixer_control) + g_return_if_fail (mixer_control); + + /* Volume slider */ + g_signal_connect_object (mixer_control, + "active-output-update", + G_CALLBACK (mixer_control_output_update_cb), + self, + G_CONNECT_DEFAULT); + g_signal_connect (self->output_vol_bar, + "value-changed", + G_CALLBACK (vol_bar_value_changed_cb), + self); + + /* Toggle details button */ + g_object_bind_property_full (self->toggle_audio_details, "active", + self->stack_audio_details, "visible-child-name", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE, + transform_toggle_stack_child_name, + NULL, NULL, NULL); + + /* Audio device selection */ + output_devices = phosh_audio_manager_get_output_devices (self->audio_manager); + gtk_list_box_bind_model (GTK_LIST_BOX (self->listbox_audio_output_devices), + G_LIST_MODEL (output_devices), + create_audio_device_row, + self, + NULL); + g_object_bind_property (output_devices, "has-devices", + self->box_audio_output_devices, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); + + input_devices = phosh_audio_manager_get_input_devices (self->audio_manager); + gtk_list_box_bind_model (GTK_LIST_BOX (self->listbox_audio_input_devices), + G_LIST_MODEL (input_devices), + create_audio_device_row, + self, + NULL); + g_object_bind_property (input_devices, "has-devices", + self->box_audio_input_devices, "visible", + G_BINDING_DEFAULT | G_BINDING_SYNC_CREATE); +} + + +PhoshAudioSettings * +phosh_audio_settings_new (void) +{ + return g_object_new (PHOSH_TYPE_AUDIO_SETTINGS, NULL); +} + + +gboolean +phosh_audio_settings_get_output_is_headphone (PhoshAudioSettings *self) +{ + g_return_val_if_fail (PHOSH_IS_AUDIO_SETTINGS (self), FALSE); + + return self->is_headphone; +} + +/** + * phosh_audio_settings_hide_details: + * @self: The audio settings widget + * + * Hides the audio settings details + */ +void +phosh_audio_settings_hide_details (PhoshAudioSettings *self) +{ + g_return_if_fail (PHOSH_IS_AUDIO_SETTINGS (self)); + + gtk_toggle_button_set_active (GTK_TOGGLE_BUTTON (self->toggle_audio_details), FALSE); +} diff --git a/src/settings/audio-settings.h b/src/settings/audio-settings.h new file mode 100644 index 000000000..431db99a7 --- /dev/null +++ b/src/settings/audio-settings.h @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_AUDIO_SETTINGS (phosh_audio_settings_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshAudioSettings, phosh_audio_settings, PHOSH, AUDIO_SETTINGS, GtkBin) + +PhoshAudioSettings *phosh_audio_settings_new (void); +gboolean phosh_audio_settings_get_output_is_headphone (PhoshAudioSettings *self); +void phosh_audio_settings_hide_details (PhoshAudioSettings *self); + +G_END_DECLS diff --git a/src/settings/channel-bar.c b/src/settings/channel-bar.c new file mode 100644 index 000000000..bf875036e --- /dev/null +++ b/src/settings/channel-bar.c @@ -0,0 +1,386 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * based on gvc-channel-bar.h from g-c-c which is + * Copyright (C) 2008 Red Hat, Inc. + */ + +#define G_LOG_DOMAIN "phosh-channel-bar" + +#include "phosh-config.h" + +#include "channel-bar.h" + +#include +#include +#include + +#define ADJUSTMENT_MAX_NORMAL PA_VOLUME_NORM +#define ADJUSTMENT_MAX_AMPLIFIED PA_VOLUME_UI_MAX +#define ADJUSTMENT_MAX (self->is_amplified ? ADJUSTMENT_MAX_AMPLIFIED : ADJUSTMENT_MAX_NORMAL) + +enum +{ + PROP_0, + PROP_IS_MUTED, + PROP_ICON_NAME, + PROP_IS_AMPLIFIED, + LAST_PROP, +}; +static GParamSpec *props[LAST_PROP]; + + +enum { + VALUE_CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +struct _PhoshChannelBar { + GtkBox parent_instance; + + GtkWidget *scale_box; + GtkWidget *image; + GtkWidget *scale; + GtkAdjustment *adjustment; + gboolean is_muted; + char *icon_name; + GtkSizeGroup *size_group; + gboolean click_lock; + gboolean is_amplified; + guint32 base_volume; +}; + +G_DEFINE_TYPE (PhoshChannelBar, phosh_channel_bar, GTK_TYPE_BOX) + + +static void +update_image (PhoshChannelBar *self) +{ + g_autoptr (GIcon) gicon = NULL; + + if (self->icon_name) { + gicon = g_themed_icon_new_with_default_fallbacks (self->icon_name); + gtk_image_set_from_gicon (GTK_IMAGE (self->image), gicon, -1); + } + + gtk_widget_set_visible (self->image, self->icon_name != NULL); +} + + +void +phosh_channel_bar_set_size_group (PhoshChannelBar *self, GtkSizeGroup *group) +{ + g_return_if_fail (PHOSH_IS_CHANNEL_BAR (self)); + + self->size_group = group; + + if (self->size_group != NULL) + gtk_size_group_add_widget (self->size_group, self->scale_box); + + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +void +phosh_channel_bar_set_icon_name (PhoshChannelBar *self, const char *name) +{ + g_return_if_fail (PHOSH_IS_CHANNEL_BAR (self)); + + if (g_strcmp0 (self->icon_name, name) == 0) + return; + + g_free (self->icon_name); + self->icon_name = g_strdup (name); + update_image (self); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +GtkAdjustment * +phosh_channel_bar_get_adjustment (PhoshChannelBar *self) +{ + g_return_val_if_fail (PHOSH_IS_CHANNEL_BAR (self), NULL); + + return self->adjustment; +} + + +static gboolean +on_scale_button_press_event (GtkWidget *widget, + GdkEventButton *event, + PhoshChannelBar *self) +{ + self->click_lock = TRUE; + + return FALSE; +} + + +static gboolean +on_scale_button_release_event (GtkWidget *widget, + GdkEventButton *event, + PhoshChannelBar *self) +{ + double value; + + self->click_lock = FALSE; + value = gtk_adjustment_get_value (self->adjustment); + + /* this means the adjustment moved away from zero and + * therefore we should unmute and set the volume. */ + phosh_channel_bar_set_is_muted (self, ((int)value == (int)0.0)); + + return FALSE; +} + + +static void +on_adjustment_value_changed (GtkAdjustment *adjustment, PhoshChannelBar *self) +{ + if (!self->is_muted || self->click_lock) + g_signal_emit (self, signals[VALUE_CHANGED], 0); +} + + +void +phosh_channel_bar_set_is_muted (PhoshChannelBar *self, gboolean is_muted) +{ + g_return_if_fail (PHOSH_IS_CHANNEL_BAR (self)); + + if (is_muted == self->is_muted) + return; + + /* Update our internal state before telling the + * front-end about our changes */ + self->is_muted = is_muted; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_MUTED]); + + if (is_muted) + gtk_adjustment_set_value (self->adjustment, 0.0); +} + + +gboolean +phosh_channel_bar_get_is_muted (PhoshChannelBar *self) +{ + g_return_val_if_fail (PHOSH_IS_CHANNEL_BAR (self), FALSE); + return self->is_muted; +} + + +void +phosh_channel_bar_set_is_amplified (PhoshChannelBar *self, gboolean amplified) +{ + g_return_if_fail (PHOSH_IS_CHANNEL_BAR (self)); + + if (self->is_amplified == amplified) + return; + + self->is_amplified = amplified; + gtk_adjustment_set_upper (self->adjustment, ADJUSTMENT_MAX); + gtk_scale_clear_marks (GTK_SCALE (self->scale)); + + if (amplified) { + g_autofree char *str = NULL; + + if (G_APPROX_VALUE (self->base_volume, floor (ADJUSTMENT_MAX_NORMAL), DBL_EPSILON)) { + str = g_strdup_printf ("%s", C_("volume", "100%")); + gtk_scale_add_mark (GTK_SCALE (self->scale), ADJUSTMENT_MAX_NORMAL, + GTK_POS_BOTTOM, str); + } else { + str = g_strdup_printf ("%s", C_("volume", "Unamplified")); + gtk_scale_add_mark (GTK_SCALE (self->scale), self->base_volume, + GTK_POS_BOTTOM, str); + /* Only show 100% if it's higher than the base volume */ + if (self->base_volume < ADJUSTMENT_MAX_NORMAL) { + str = g_strdup_printf ("%s", C_("volume", "100%")); + gtk_scale_add_mark (GTK_SCALE (self->scale), ADJUSTMENT_MAX_NORMAL, + GTK_POS_BOTTOM, str); + } + } + + /* Ideally we would use baseline alignment for all + * these widgets plus the scale but neither GtkScale + * nor GtkSwitch support baseline alignment yet. */ + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_AMPLIFIED]); +} + + +void +phosh_channel_bar_set_base_volume (PhoshChannelBar *self, pa_volume_t base_volume) +{ + g_return_if_fail (PHOSH_IS_CHANNEL_BAR (self)); + + if (base_volume == 0) { + self->base_volume = ADJUSTMENT_MAX_NORMAL; + return; + } + + /* Note that you need to call _is_amplified() afterwards to update the marks */ + self->base_volume = base_volume; +} + + +static void +phosh_channel_bar_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshChannelBar *self = PHOSH_CHANNEL_BAR (object); + + switch (prop_id) { + case PROP_IS_MUTED: + phosh_channel_bar_set_is_muted (self, g_value_get_boolean (value)); + break; + case PROP_ICON_NAME: + phosh_channel_bar_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_IS_AMPLIFIED: + phosh_channel_bar_set_is_amplified (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + + +static void +phosh_channel_bar_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshChannelBar *self = PHOSH_CHANNEL_BAR (object); + + switch (prop_id) { + case PROP_IS_MUTED: + g_value_set_boolean (value, self->is_muted); + break; + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_IS_AMPLIFIED: + g_value_set_boolean (value, self->is_amplified); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + + +static void +phosh_channel_bar_finalize (GObject *object) +{ + PhoshChannelBar *self = PHOSH_CHANNEL_BAR (object); + + g_free (self->icon_name); + + G_OBJECT_CLASS (phosh_channel_bar_parent_class)->finalize (object); +} + + +static void +phosh_channel_bar_class_init (PhoshChannelBarClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_channel_bar_finalize; + object_class->set_property = phosh_channel_bar_set_property; + object_class->get_property = phosh_channel_bar_get_property; + + /** + * PhoshChannelBar:is-muted: + * + * Whether the stream is muted + */ + props[PROP_IS_MUTED] = + g_param_spec_boolean ("is-muted", "", "", + FALSE, + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT); + /** + * PhoshChannelBar:icon-name: + * + * The name of icon to display for this stream + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT); + /** + * PhoshChannelBar:is-amplified: + * + * Whether the stream is digitally amplified + */ + props[PROP_IS_AMPLIFIED] = + g_param_spec_boolean ("is-amplified", "", "", + FALSE, + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_CONSTRUCT); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + signals[VALUE_CHANGED] = g_signal_new ("value-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/channel-bar.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshChannelBar, adjustment); + gtk_widget_class_bind_template_child (widget_class, PhoshChannelBar, scale_box); + gtk_widget_class_bind_template_child (widget_class, PhoshChannelBar, image); + gtk_widget_class_bind_template_child (widget_class, PhoshChannelBar, scale); + gtk_widget_class_bind_template_callback (widget_class, on_adjustment_value_changed); + gtk_widget_class_bind_template_callback (widget_class, on_scale_button_press_event); + gtk_widget_class_bind_template_callback (widget_class, on_scale_button_release_event); + + gtk_widget_class_set_css_name (widget_class, "phosh-channel-bar"); +} + + +static void +phosh_channel_bar_init (PhoshChannelBar *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + self->base_volume = ADJUSTMENT_MAX_NORMAL; + gtk_adjustment_set_upper (self->adjustment, ADJUSTMENT_MAX_NORMAL); + gtk_adjustment_set_step_increment (self->adjustment, ADJUSTMENT_MAX_NORMAL / 100.0); + gtk_adjustment_set_page_increment (self->adjustment, ADJUSTMENT_MAX_NORMAL / 10.0); + + gtk_widget_add_events (self->scale, GDK_SCROLL_MASK); +} + + +GtkWidget * +phosh_channel_bar_new (void) +{ + return g_object_new (PHOSH_TYPE_CHANNEL_BAR, + "icon-name", "audio-speakers-symbolic", + NULL); +} + + +double +phosh_channel_bar_get_volume (PhoshChannelBar *self) +{ + g_return_val_if_fail (PHOSH_IS_CHANNEL_BAR (self), 0.0); + + return gtk_adjustment_get_value (self->adjustment); +} diff --git a/src/settings/channel-bar.h b/src/settings/channel-bar.h new file mode 100644 index 000000000..c323e7e0f --- /dev/null +++ b/src/settings/channel-bar.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * based on Phosh-channel-bar.h from g-c-c which is + * Copyright (C) 2008 Red Hat, Inc. + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_CHANNEL_BAR (phosh_channel_bar_get_type ()) +G_DECLARE_FINAL_TYPE (PhoshChannelBar, phosh_channel_bar, PHOSH, CHANNEL_BAR, GtkBox) + +GtkWidget * phosh_channel_bar_new (void); + +void phosh_channel_bar_set_name (PhoshChannelBar *bar, const char *name); +void phosh_channel_bar_set_icon_name (PhoshChannelBar *bar, const char *icon_name); +void phosh_channel_bar_set_low_icon_name (PhoshChannelBar *bar, const char *icon_name); +void phosh_channel_bar_set_high_icon_name (PhoshChannelBar *bar, const char *icon_name); + +void phosh_channel_bar_set_orientation (PhoshChannelBar *bar, + GtkOrientation orientation); +GtkOrientation phosh_channel_bar_get_orientation (PhoshChannelBar *bar); + +GtkAdjustment *phosh_channel_bar_get_adjustment (PhoshChannelBar *bar); + +gboolean phosh_channel_bar_get_is_muted (PhoshChannelBar *bar); +void phosh_channel_bar_set_is_muted (PhoshChannelBar *bar, gboolean is_muted); +gboolean phosh_channel_bar_get_show_mute (PhoshChannelBar *bar); +void phosh_channel_bar_set_show_mute (PhoshChannelBar *bar, gboolean show_mute); +void phosh_channel_bar_set_size_group (PhoshChannelBar *bar, GtkSizeGroup *group); +void phosh_channel_bar_set_is_amplified (PhoshChannelBar *bar, gboolean amplified); +void phosh_channel_bar_set_base_volume (PhoshChannelBar *bar, guint32 base_volume); +gboolean phosh_channel_bar_get_ellipsize (PhoshChannelBar *bar); +void phosh_channel_bar_set_ellipsize (PhoshChannelBar *bar, gboolean ellipsized); + +double phosh_channel_bar_get_volume (PhoshChannelBar *self); + +G_END_DECLS diff --git a/src/settings/meson.build b/src/settings/meson.build new file mode 100644 index 000000000..c12d4d3f8 --- /dev/null +++ b/src/settings/meson.build @@ -0,0 +1,7 @@ +phosh_settings_widgets_headers = files('audio-device-row.h', 'audio-settings.h') + +phosh_settings_widgets_sources = files( + 'audio-device-row.c', + 'audio-settings.c', + 'channel-bar.c', +) diff --git a/src/shell-priv.h b/src/shell-priv.h new file mode 100644 index 000000000..83d8d6152 --- /dev/null +++ b/src/shell-priv.h @@ -0,0 +1,141 @@ +/* + * Copyright (C) 2025 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "ambient.h" +#include "app-tracker.h" +#include "background-manager.h" +#include "battery-manager.h" +#include "brightness-manager.h" +#include "bt-manager.h" +#include "calls-manager.h" +#include "connectivity-manager.h" +#include "docked-manager.h" +#include "emergency-calls-manager.h" +#include "feedback-manager.h" +#include "gtk-mount-manager.h" +#include "hks-manager.h" +#include "layout-manager.h" +#include "location-manager.h" +#include "plugin-shell.h" +#include "monitor/monitor.h" +#include "osk-manager.h" +#include "rotation-manager.h" +#include "screen-saver-manager.h" +#include "splash-manager.h" +#include "style-manager.h" +#include "toplevel-manager.h" +#include "torch-manager.h" +#include "vpn-manager.h" + +#include + +G_BEGIN_DECLS + +/** + * PhoshShellStateFlags: + * @PHOSH_STATE_NONE: No other state + * @PHOSH_STATE_MODAL_SYSTEM_PROMPT: any modal prompt shown + * @PHOSH_STATE_BLANKED: primary display off + * @PHOSH_STATE_LOCKED: displays locked + * @PHOSH_STATE_SETTINGS: settings menu unfolded from top bar + * @PHOSH_STATE_OVERVIEW: overview unfolded from bottom bar + * + * These flags are used to keep track of the state + * the #PhoshShell is in. + */ +typedef enum { + PHOSH_STATE_NONE = 0, + PHOSH_STATE_MODAL_SYSTEM_PROMPT = 1 << 0, + PHOSH_STATE_BLANKED = 1 << 1, + PHOSH_STATE_LOCKED = 1 << 2, + PHOSH_STATE_SETTINGS = 1 << 3, + PHOSH_STATE_OVERVIEW = 1 << 4, +} PhoshShellStateFlags; + + +/** + * PhoshShellDebugFlags + * @PHOSH_SHELL_DEBUG_FLAG_NONE: No debug flags + * @PHOSH_SHELL_DEBUG_FLAG_ALWAYS_SPLASH: always use splash (even when docked) + * @PHOSH_SHELL_DEBUG_FLAG_FAKE_BUILTIN: When calculatiog layout treat the first + * virtual output like a built-in output. + * @PHOSH_SHELL_DEBUG_BACKLIGHT_NON_LINEAR: Assume backlight uses non-linear scale + * + * These flags are to enable/disable debugging features. + */ +typedef enum { + PHOSH_SHELL_DEBUG_FLAG_NONE = 0, + PHOSH_SHELL_DEBUG_FLAG_ALWAYS_SPLASH = 1 << 0, + PHOSH_SHELL_DEBUG_FLAG_FAKE_BUILTIN = 1 << 1, + PHOSH_SHELL_DEBUG_BACKLIGHT_NON_LINEAR = 1 << 2, +} PhoshShellDebugFlags; + + +PhoshShellDebugFlags phosh_shell_get_debug_flags (void); +void phosh_shell_get_area (PhoshShell *self, int *width, int *height); +void phosh_shell_set_locked (PhoshShell *self, gboolean locked); +void phosh_shell_lock (PhoshShell *self); +void phosh_shell_unlock (PhoshShell *self); +void phosh_shell_set_primary_monitor (PhoshShell *self, PhoshMonitor *monitor); + +/* Created by the shell on startup */ +PhoshAmbient *phosh_shell_get_ambient (PhoshShell *self); +PhoshAppTracker *phosh_shell_get_app_tracker (PhoshShell *self); +PhoshBackgroundManager *phosh_shell_get_background_manager (PhoshShell *self); +PhoshBrightnessManager *phosh_shell_get_brightness_manager (PhoshShell *self); +PhoshCallsManager *phosh_shell_get_calls_manager (PhoshShell *self); +PhoshFeedbackManager *phosh_shell_get_feedback_manager (PhoshShell *self); +PhoshGtkMountManager *phosh_shell_get_gtk_mount_manager (PhoshShell *self); +PhoshLayoutManager *phosh_shell_get_layout_manager (PhoshShell *self); +PhoshModeManager *phosh_shell_get_mode_manager (PhoshShell *self); +PhoshSplashManager * phosh_shell_get_splash_manager (PhoshShell *self); +PhoshStyleManager *phosh_shell_get_style_manager (PhoshShell *self); +PhoshToplevelManager *phosh_shell_get_toplevel_manager (PhoshShell *self); +PhoshScreenSaverManager *phosh_shell_get_screen_saver_manager (PhoshShell *self); +/* Created on the fly */ +PhoshBtManager *phosh_shell_get_bt_manager (PhoshShell *self); +PhoshBatteryManager *phosh_shell_get_battery_manager (PhoshShell *self); +PhoshConnectivityManager *phosh_shell_get_connectivity_manager (PhoshShell *self); +PhoshDockedManager *phosh_shell_get_docked_manager (PhoshShell *self); +PhoshHksManager *phosh_shell_get_hks_manager (PhoshShell *self); +PhoshLocationManager *phosh_shell_get_location_manager (PhoshShell *self); +PhoshOskManager *phosh_shell_get_osk_manager (PhoshShell *self); +PhoshRotationManager *phosh_shell_get_rotation_manager (PhoshShell *self); +PhoshTorchManager *phosh_shell_get_torch_manager (PhoshShell *self); +PhoshVpnManager *phosh_shell_get_vpn_manager (PhoshShell *self); +PhoshEmergencyCallsManager *phosh_shell_get_emergency_calls_manager (PhoshShell *self); + +void phosh_shell_enable_power_save (PhoshShell *self, gboolean enable); +gboolean phosh_shell_started_by_display_manager(PhoshShell *self); +gboolean phosh_shell_is_startup_finished (PhoshShell *self); +void phosh_shell_add_global_keyboard_action_entries (PhoshShell *self, + const GActionEntry *actions, + gint n_entries, + gpointer user_data); +void phosh_shell_remove_global_keyboard_action_entries (PhoshShell *self, + GStrv action_names); +gboolean phosh_shell_is_session_active (PhoshShell *self); +GdkAppLaunchContext *phosh_shell_get_app_launch_context (PhoshShell *self); +PhoshShellStateFlags phosh_shell_get_state (PhoshShell *self); +void phosh_shell_set_state (PhoshShell *self, PhoshShellStateFlags state, gboolean enabled); +gboolean phosh_shell_get_show_splash (PhoshShell *self); +gboolean phosh_shell_get_docked (PhoshShell *self); +gboolean phosh_shell_get_blanked (PhoshShell *self); +gboolean phosh_shell_activate_action (PhoshShell *self, + const char *action, + GVariant *parameter); + +void phosh_shell_set_bg_alpha (PhoshShell *self, double alpha); +void phosh_shell_show_osd (PhoshShell *self, + const char *connector, + const char *icon, + const char *label, + double level, + double max_level); + +G_END_DECLS diff --git a/src/shell.c b/src/shell.c new file mode 100644 index 000000000..b836cd361 --- /dev/null +++ b/src/shell.c @@ -0,0 +1,2833 @@ +/* + * Copyright (C) 2018 Purism SPC + * 2023-2024 The Phosh Developers + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Once based on maynard's panel which is + * Copyright (C) 2014 Collabora Ltd. * + * Author: Jonny Lamb + */ + +#define G_LOG_DOMAIN "phosh-shell" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "phosh-config.h" +#include "ambient.h" +#include "background.h" +#include "brightness-manager.h" +#include "drag-surface.h" +#include "debug-control.h" +#include "shell-priv.h" +#include "app-tracker.h" +#include "battery-info.h" +#include "background-manager.h" +#include "bt-info.h" +#include "bt-manager.h" +#include "connectivity-info.h" +#include "connectivity-manager.h" +#include "calls-manager.h" +#include "cell-broadcast-manager.h" +#include "docked-info.h" +#include "docked-manager.h" +#include "emergency-calls-manager.h" +#include "fader.h" +#include "feedbackinfo.h" +#include "feedback-manager.h" +#include "gnome-shell-manager.h" +#include "gtk-mount-manager.h" +#include "hks-info.h" +#include "home.h" +#include "idle-manager.h" +#include "keyboard-events.h" +#include "launcher-entry-manager.h" +#include "layersurface-priv.h" +#include "location-info.h" +#include "layout-manager.h" +#include "location-manager.h" +#include "lockscreen-manager-priv.h" +#include "default-media-player.h" +#include "mode-manager.h" +#include "monitor-manager.h" +#include "monitor/monitor.h" +#include "mount-manager.h" +#include "osd-window.h" +#include "power-menu-manager.h" +#include "revealer.h" +#include "settings.h" +#include "system-modal-dialog.h" +#include "mpris-manager.h" +#include "network-auth-manager.h" +#include "notifications/notify-manager.h" +#include "notifications/notification-banner.h" +#include "osk-manager.h" +#include "password-entry.h" +#include "phosh-private-client-protocol.h" +#include "phosh-wayland.h" +#include "plugin-loader.h" +#include "polkit-auth-agent.h" +#include "portal-access-manager.h" +#include "proximity.h" +#include "quick-setting.h" +#include "run-command-manager.h" +#include "rotateinfo.h" +#include "rotation-manager.h" +#include "sensor-proxy-manager.h" +#include "screen-saver-manager.h" +#include "screenshot-manager.h" +#include "session-manager.h" +#include "splash-manager.h" +#include "style-manager.h" +#include "suspend-manager.h" +#include "system-prompter.h" +#include "top-panel.h" +#include "top-panel-bg.h" +#include "torch-manager.h" +#include "torch-info.h" +#include "udev-manager.h" +#include "util.h" +#include "vpn-info.h" +#include "wifi-info.h" +#include "wwan-info.h" +#include "wwan/phosh-wwan-ofono.h" +#include "wwan/phosh-wwan-mm.h" +#include "wall-clock.h" + +#include "phosh-settings-enums.h" + +#define WWAN_BACKEND_KEY "wwan-backend" +#define OSD_HIDE_TIMEOUT 1 /* seconds */ + +/** + * PhoshShell: + * + * The shell singleton + * + * #PhoshShell is responsible for instantiating the GUI + * parts of the shell#PhoshTopPanel, #PhoshHome,… and the managers that + * interface with DBus #PhoshMonitorManager, #PhoshFeedbackManager, … + * and coordinates between them. + */ + +enum { + PROP_0, + PROP_LOCKED, + PROP_DOCKED, + PROP_BUILTIN_MONITOR, + PROP_PRIMARY_MONITOR, + PROP_SHELL_STATE, + PROP_OVERVIEW_VISIBLE, + PROP_LOG_DOMAINS, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + READY, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +static PhoshShellDebugFlags debug_flags; + +typedef struct { + PhoshDragSurface *top_panel; + PhoshDragSurface *home; + gboolean overview_visible; + GPtrArray *faders; /* for final fade out */ + GStrv log_domains; + + PhoshOsdWindow *osd; + gint osd_timeoutid; + gboolean osd_continue; + + GtkWidget *notification_banner; + + PhoshUdevManager *udev_manager; + PhoshAppTracker *app_tracker; + PhoshSessionManager *session_manager; + PhoshBackgroundManager *background_manager; + PhoshCallsManager *calls_manager; + PhoshMonitor *primary_monitor; + PhoshMonitor *builtin_monitor; + PhoshMonitorManager *monitor_manager; + PhoshLockscreenManager *lockscreen_manager; + PhoshIdleManager *idle_manager; + PhoshOskManager *osk_manager; + PhoshToplevelManager *toplevel_manager; + PhoshWifiManager *wifi_manager; + PhoshPolkitAuthAgent *polkit_auth_agent; + PhoshScreenSaverManager *screen_saver_manager; + PhoshScreenshotManager *screenshot_manager; + PhoshNotifyManager *notify_manager; + PhoshFeedbackManager *feedback_manager; + PhoshBatteryManager *battery_manager; + PhoshBtManager *bt_manager; + PhoshMountManager *mount_manager; + PhoshWWan *wwan; + PhoshTorchManager *torch_manager; + PhoshModeManager *mode_manager; + PhoshDockedManager *docked_manager; + PhoshGtkMountManager *gtk_mount_manager; + PhoshHksManager *hks_manager; + PhoshKeyboardEvents *keyboard_events; + PhoshLocationManager *location_manager; + PhoshGnomeShellManager *gnome_shell_manager; + PhoshSplashManager *splash_manager; + PhoshRunCommandManager *run_command_manager; + PhoshNetworkAuthManager *network_auth_manager; + PhoshVpnManager *vpn_manager; + PhoshPortalAccessManager *portal_access_manager; + PhoshSuspendManager *suspend_manager; + PhoshEmergencyCallsManager *emergency_calls_manager; + PhoshPowerMenuManager *power_menu_manager; + PhoshLayoutManager *layout_manager; + PhoshStyleManager *style_manager; + PhoshLauncherEntryManager *launcher_entry_manager; + PhoshCellBroadcastManager *cell_broadcast_manager; + PhoshConnectivityManager *connectivity_manager; + PhoshMprisManager *mpris_manager; + PhoshBrightnessManager *brightness_manager; + PhoshDebugControl *debug_control; + + /* sensors */ + PhoshSensorProxyManager *sensor_proxy_manager; + PhoshProximity *proximity; + PhoshAmbient *ambient; + PhoshRotationManager *rotation_manager; + + gboolean startup_finished; + guint startup_finished_id; + + GSimpleActionGroup *action_map; + + /* Mirrors PhoshLockscreenManager's locked property */ + gboolean locked; + + /* Mirrors PhoshDockedManager's docked property */ + gboolean docked; + + PhoshShellStateFlags shell_state; + + GSettings *settings; +} PhoshShellPrivate; + +static void phosh_shell_action_group_iface_init (GActionGroupInterface *iface); +static void phosh_shell_action_map_iface_init (GActionMapInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshShell, phosh_shell, G_TYPE_OBJECT, + G_ADD_PRIVATE (PhoshShell) + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, phosh_shell_action_group_iface_init) + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_MAP, phosh_shell_action_map_iface_init) + ) + +static void +on_top_panel_activated (PhoshShell *self, + PhoshTopPanel *window) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (priv->top_panel)); + phosh_top_panel_toggle_fold (PHOSH_TOP_PANEL (priv->top_panel)); +} + + +static void +update_top_level_layer (PhoshShell *self) +{ + PhoshShellStateFlags state; + PhoshShellPrivate *priv; + guint32 layer, current; + gboolean use_top_layer; + + priv = phosh_shell_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + if (priv->top_panel == NULL) + return; + + g_return_if_fail (PHOSH_IS_TOP_PANEL (priv->top_panel)); + state = phosh_shell_get_state (self); + + /* When the proximity fader is on we want to remove the top-panel from the + overlay layer since it uses an exclusive zone and hence the fader is + drawn below that top-panel. This can be dropped once layer-shell allows + to specify the z-level */ + use_top_layer = priv->proximity && phosh_proximity_has_fader (priv->proximity); + if (use_top_layer) + goto set_layer; + + /* We want the top-bar on the lock screen */ + use_top_layer = !phosh_shell_get_locked (self); + if (use_top_layer) + goto set_layer; + + /* If there's a modal dialog make sure it can extend over the top-panel */ + use_top_layer = !!(state & PHOSH_STATE_MODAL_SYSTEM_PROMPT); + + set_layer: + layer = use_top_layer ? ZWLR_LAYER_SHELL_V1_LAYER_TOP : ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY; + g_object_get (priv->top_panel, "layer", ¤t, NULL); + if (current == layer) + return; + + g_debug ("Moving top-panel to %s layer", use_top_layer ? "top" : "overlay"); + phosh_top_panel_set_layer (PHOSH_TOP_PANEL (priv->top_panel), layer); +} + + +static void +on_proximity_fader_changed (PhoshShell *self) +{ + update_top_level_layer (self); +} + + +static void +on_top_panel_state_changed (PhoshShell *self, GParamSpec *pspec, PhoshTopPanel *top_panel) +{ + PhoshShellPrivate *priv; + PhoshTopPanelState state; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_TOP_PANEL (top_panel)); + + priv = phosh_shell_get_instance_private (self); + + state = phosh_top_panel_get_state (PHOSH_TOP_PANEL (priv->top_panel)); + phosh_shell_set_state (self, PHOSH_STATE_SETTINGS, state == PHOSH_TOP_PANEL_STATE_UNFOLDED); +} + +static void +update_top_bar_transparency (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + PhoshHomeState state; + + if (!priv->top_panel) + return; + + if (priv->locked) { + phosh_top_panel_set_bar_transparent (PHOSH_TOP_PANEL (priv->top_panel), TRUE); + return; + } + + state = phosh_home_get_state (PHOSH_HOME (priv->home)); + phosh_top_panel_set_bar_transparent (PHOSH_TOP_PANEL (priv->top_panel), + (state != PHOSH_HOME_STATE_FOLDED)); +} + + +static void +on_home_state_changed (PhoshShell *self, GParamSpec *pspec, PhoshHome *home) +{ + PhoshShellPrivate *priv; + PhoshHomeState state; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_HOME (home)); + priv = phosh_shell_get_instance_private (self); + + update_top_bar_transparency (self); + + state = phosh_home_get_state (PHOSH_HOME (priv->home)); + phosh_shell_set_state (self, PHOSH_STATE_OVERVIEW, state == PHOSH_HOME_STATE_UNFOLDED); +} + + +static void +on_primary_monitor_power_mode_changed (PhoshShell *self, GParamSpec *pspec, PhoshMonitor *monitor) +{ + PhoshMonitorPowerSaveMode mode; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + + g_object_get (monitor, "power-mode", &mode, NULL); + + phosh_shell_set_state (self, PHOSH_STATE_BLANKED, mode == PHOSH_MONITOR_POWER_SAVE_MODE_OFF); +} + + +static void +on_primary_monitor_configured (PhoshShell *self, PhoshMonitor *monitor) +{ + PhoshShellPrivate *priv; + int height; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + priv = phosh_shell_get_instance_private (self); + + phosh_shell_get_area (self, NULL, &height); + phosh_layer_surface_set_size (PHOSH_LAYER_SURFACE (priv->top_panel), -1, height); +} + + +static void +setup_primary_monitor_signal_handlers (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->primary_monitor) + return; + + g_signal_connect_swapped (priv->primary_monitor, + "notify::power-mode", + G_CALLBACK (on_primary_monitor_power_mode_changed), + self); + + g_signal_connect_object (priv->primary_monitor, "configured", + G_CALLBACK (on_primary_monitor_configured), + self, + G_CONNECT_SWAPPED); + + if (phosh_monitor_is_configured (priv->primary_monitor)) + on_primary_monitor_configured (self, priv->primary_monitor); +} + + +static void +panels_create (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshMonitor *monitor; + PhoshAppGrid *app_grid; + guint32 top_layer; + + monitor = phosh_shell_get_primary_monitor (self); + g_return_if_fail (monitor); + + top_layer = priv->locked ? ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY : ZWLR_LAYER_SHELL_V1_LAYER_TOP; + + /* Top panel */ + priv->top_panel = PHOSH_DRAG_SURFACE (phosh_top_panel_new ( + phosh_wayland_get_zwlr_layer_shell_v1 (wl), + phosh_wayland_get_zphoc_layer_shell_effects_v1 (wl), + monitor, + top_layer)); + gtk_widget_set_visible (GTK_WIDGET (priv->top_panel), TRUE); + + /* Home is created after the top-panel so it honors its exclusive zone */ + priv->home = PHOSH_DRAG_SURFACE (phosh_home_new (phosh_wayland_get_zwlr_layer_shell_v1 (wl), + phosh_wayland_get_zphoc_layer_shell_effects_v1 (wl), + monitor)); + g_object_bind_property (self, "overview-visible", priv->home, "visible", G_BINDING_SYNC_CREATE); + + g_signal_connect_swapped (priv->top_panel, + "activated", + G_CALLBACK (on_top_panel_activated), + self); + + g_signal_connect_swapped (priv->top_panel, + "notify::state", + G_CALLBACK (on_top_panel_state_changed), + self); + + g_signal_connect_swapped (priv->home, + "notify::state", + G_CALLBACK (on_home_state_changed), + self); + + app_grid = phosh_overview_get_app_grid (phosh_home_get_overview (PHOSH_HOME (priv->home))); + g_object_bind_property (priv->docked_manager, + "enabled", + app_grid, + "filter-adaptive", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); +} + + +static void +panels_dispose (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_clear_pointer (&priv->top_panel, phosh_cp_widget_destroy); + g_clear_pointer (&priv->home, phosh_cp_widget_destroy); +} + + +static void +set_locked (PhoshShell *self, gboolean locked) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (priv->locked == locked) + return; + + priv->locked = locked; + phosh_shell_set_state (self, PHOSH_STATE_LOCKED, priv->locked); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOCKED]); + + /* Hide settings on screen lock, otherwise the user just sees the settings when + unblanking the screen which can be confusing */ + if (priv->top_panel) + phosh_top_panel_fold (PHOSH_TOP_PANEL (priv->top_panel)); + + update_top_level_layer (self); + update_top_bar_transparency (self); +} + + +static void +phosh_shell_set_log_domains (PhoshShell *self, const char *const *log_domains) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_SHELL (self)); + + if (!priv->log_domains && !log_domains) + return; + + if (priv->log_domains && log_domains && + g_strv_equal ((const char *const *)priv->log_domains, log_domains)) + return; + + g_strfreev (priv->log_domains); + priv->log_domains = g_strdupv ((GStrv)log_domains); + + g_log_writer_default_set_debug_domains (log_domains); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_LOG_DOMAINS]); +} + + +static void +phosh_shell_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshShell *self = PHOSH_SHELL (object); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + switch (property_id) { + case PROP_LOCKED: + /* Only written by lockscreen manager on property sync */ + set_locked (self, g_value_get_boolean (value)); + break; + case PROP_DOCKED: + /* Only written by docked manager on property sync */ + priv->docked = g_value_get_boolean (value); + break; + case PROP_PRIMARY_MONITOR: + phosh_shell_set_primary_monitor (self, g_value_get_object (value)); + break; + case PROP_OVERVIEW_VISIBLE: + priv->overview_visible = g_value_get_boolean (value); + break; + case PROP_LOG_DOMAINS: + phosh_shell_set_log_domains (self, g_value_get_boxed (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_shell_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshShell *self = PHOSH_SHELL (object); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + switch (property_id) { + case PROP_LOCKED: + g_value_set_boolean (value, phosh_shell_get_locked (self)); + break; + case PROP_DOCKED: + g_value_set_boolean (value, phosh_shell_get_docked (self)); + break; + case PROP_BUILTIN_MONITOR: + g_value_set_object (value, phosh_shell_get_builtin_monitor (self)); + break; + case PROP_PRIMARY_MONITOR: + g_value_set_object (value, phosh_shell_get_primary_monitor (self)); + break; + case PROP_SHELL_STATE: + g_value_set_flags (value, priv->shell_state); + break; + case PROP_OVERVIEW_VISIBLE: + g_value_set_boolean (value, priv->overview_visible); + break; + case PROP_LOG_DOMAINS: + g_value_set_boxed (value, priv->log_domains); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_shell_dispose (GObject *object) +{ + PhoshShell *self = PHOSH_SHELL (object); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_clear_handle_id (&priv->startup_finished_id, g_source_remove); + + panels_dispose (self); + g_clear_pointer (&priv->faders, g_ptr_array_unref); + + g_clear_pointer (&priv->notification_banner, phosh_cp_widget_destroy); + + /* dispose managers in opposite order of declaration */ + g_clear_object (&priv->debug_control); + g_clear_object (&priv->brightness_manager); + g_clear_object (&priv->mpris_manager); + g_clear_object (&priv->connectivity_manager); + g_clear_object (&priv->cell_broadcast_manager); + g_clear_object (&priv->launcher_entry_manager); + g_clear_object (&priv->power_menu_manager); + g_clear_object (&priv->emergency_calls_manager); + g_clear_object (&priv->portal_access_manager); + g_clear_object (&priv->vpn_manager); + g_clear_object (&priv->network_auth_manager); + g_clear_object (&priv->run_command_manager); + g_clear_object (&priv->splash_manager); + g_clear_object (&priv->screenshot_manager); + g_clear_object (&priv->calls_manager); + g_clear_object (&priv->location_manager); + g_clear_object (&priv->hks_manager); + g_clear_object (&priv->gtk_mount_manager); + g_clear_object (&priv->docked_manager); + g_clear_object (&priv->mode_manager); + g_clear_object (&priv->torch_manager); + g_clear_object (&priv->gnome_shell_manager); + g_clear_object (&priv->wwan); + g_clear_object (&priv->mount_manager); + g_clear_object (&priv->battery_manager); + g_clear_object (&priv->bt_manager); + g_clear_object (&priv->feedback_manager); + g_clear_object (&priv->notify_manager); + g_clear_object (&priv->screen_saver_manager); + g_clear_object (&priv->polkit_auth_agent); + g_clear_object (&priv->wifi_manager); + g_clear_object (&priv->toplevel_manager); + g_clear_object (&priv->osk_manager); + g_clear_object (&priv->idle_manager); + g_clear_object (&priv->lockscreen_manager); + g_clear_object (&priv->monitor_manager); + g_clear_object (&priv->builtin_monitor); + g_clear_object (&priv->primary_monitor); + g_clear_object (&priv->background_manager); + g_clear_object (&priv->keyboard_events); + g_clear_object (&priv->app_tracker); + g_clear_object (&priv->suspend_manager); + g_clear_object (&priv->layout_manager); + g_clear_object (&priv->style_manager); + g_clear_object (&priv->udev_manager); + + /* sensors */ + g_clear_object (&priv->proximity); + g_clear_object (&priv->rotation_manager); + g_clear_object (&priv->sensor_proxy_manager); + + phosh_system_prompter_unregister (); + g_clear_object (&priv->session_manager); + + g_clear_object (&priv->action_map); + g_clear_object (&priv->settings); + + G_OBJECT_CLASS (phosh_shell_parent_class)->dispose (object); +} + + +static void +phosh_shell_finalize (GObject *object) +{ + PhoshShell *self = PHOSH_SHELL (object); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_clear_pointer (&priv->log_domains, g_strfreev); + + cui_uninit (); + G_OBJECT_CLASS (phosh_shell_parent_class)->finalize (object); +} + + +static void +on_num_toplevels_changed (PhoshShell *self, + GParamSpec *pspec, + PhoshToplevelManager *toplevel_manager) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (toplevel_manager)); + + priv = phosh_shell_get_instance_private (self); + /* all toplevels gone, show the overview */ + if (!phosh_toplevel_manager_get_num_toplevels (toplevel_manager)) + phosh_home_set_state (PHOSH_HOME (priv->home), PHOSH_HOME_STATE_UNFOLDED); +} + + +static void +on_toplevel_added (PhoshShell *self, PhoshToplevel *unused, PhoshToplevelManager *toplevel_manager) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (toplevel_manager)); + + priv = phosh_shell_get_instance_private (self); + if (phosh_toplevel_manager_get_num_toplevels (toplevel_manager) == 1) + phosh_home_set_state (PHOSH_HOME (priv->home), PHOSH_HOME_STATE_FOLDED); +} + + +static void +on_new_notification (PhoshShell *self, + PhoshNotification *notification, + PhoshNotifyManager *manager) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_NOTIFICATION (notification)); + g_return_if_fail (PHOSH_IS_NOTIFY_MANAGER (manager)); + + priv = phosh_shell_get_instance_private (self); + + /* Clear existing banner */ + g_clear_pointer (&priv->notification_banner, phosh_cp_widget_destroy); + if (phosh_notify_manager_get_show_notification_banner (manager, notification) && + phosh_top_panel_get_state (PHOSH_TOP_PANEL (priv->top_panel)) == PHOSH_TOP_PANEL_STATE_FOLDED && + !priv->locked) { + priv->notification_banner = phosh_notification_banner_new (notification); + g_signal_connect (priv->notification_banner, + "destroy", + G_CALLBACK (gtk_widget_destroyed), + &priv->notification_banner); + + gtk_widget_set_visible (GTK_WIDGET (priv->notification_banner), TRUE); + } +} + + +static void +on_pb_long_press (PhoshShell *self) +{ + g_return_if_fail (PHOSH_IS_SHELL (self)); + + g_action_group_activate_action (G_ACTION_GROUP (self), "power.toggle-menu", NULL); +} + + +static void +on_notification_activated (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + + priv = phosh_shell_get_instance_private (self); + + phosh_top_panel_fold (PHOSH_TOP_PANEL (priv->top_panel)); +} + + +static gboolean +on_fade_out_timeout (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), G_SOURCE_REMOVE); + + priv = phosh_shell_get_instance_private (self); + + /* kill all faders if we time out */ + priv->faders = g_ptr_array_remove_range (priv->faders, 0, priv->faders->len); + + return G_SOURCE_REMOVE; +} + + +/* {{{ OSD */ + +static gboolean +on_osd_timeout (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + gboolean ret; + + ret = priv->osd_continue ? G_SOURCE_CONTINUE : G_SOURCE_REMOVE; + if (!priv->osd_continue) { + g_debug ("Closing osd"); + priv->osd_timeoutid = 0; + if (priv->osd) + gtk_widget_destroy (GTK_WIDGET (priv->osd)); + } + priv->osd_continue = FALSE; + return ret; +} + + +static void +on_osd_destroyed (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + priv->osd = NULL; + g_clear_handle_id (&priv->osd_timeoutid, g_source_remove); +} + +/* }}} */ + +static void +notify_compositor_up_state (PhoshShell *self, enum phosh_private_shell_state state) +{ + struct phosh_private *phosh_private; + + g_debug ("Notify compositor state: %d", state); + + phosh_private = phosh_wayland_get_phosh_private (phosh_wayland_get_default ()); + if (phosh_private && + phosh_private_get_version (phosh_private) >= PHOSH_PRIVATE_SET_SHELL_STATE_SINCE_VERSION) + phosh_private_set_shell_state (phosh_private, state); +} + + +static void +on_startup_finished (gpointer data) +{ + PhoshShell *self = PHOSH_SHELL (data); + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + notify_compositor_up_state (self, PHOSH_PRIVATE_SHELL_STATE_UP); + + priv->startup_finished_id = 0; +} + + +static gboolean +setup_idle_cb (PhoshShell *self) +{ + g_autoptr (GError) err = NULL; + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + priv->debug_control = phosh_debug_control_new (); + priv->app_tracker = phosh_app_tracker_new (); + phosh_toplevel_manager_set_app_tracker (priv->toplevel_manager, priv->app_tracker); + priv->splash_manager = phosh_splash_manager_new (priv->app_tracker); + priv->session_manager = phosh_session_manager_new (); + priv->mode_manager = phosh_mode_manager_new (); + priv->wifi_manager = phosh_wifi_manager_new (); + /* Connecivity manager needs Wi-Fi manager: */ + priv->connectivity_manager = phosh_connectivity_manager_new (); + + priv->sensor_proxy_manager = phosh_sensor_proxy_manager_new (&err); + if (priv->sensor_proxy_manager) + priv->ambient = phosh_ambient_new (priv->sensor_proxy_manager); + else + g_message ("Failed to connect to sensor-proxy: %s", err->message); + + priv->layout_manager = phosh_layout_manager_new (); + /* PhoshHome needs the background manager */ + priv->background_manager = phosh_background_manager_new (); + panels_create (self); + + g_signal_connect_object (priv->toplevel_manager, + "notify::num-toplevels", + G_CALLBACK (on_num_toplevels_changed), + self, + G_CONNECT_SWAPPED); + on_num_toplevels_changed (self, NULL, priv->toplevel_manager); + + g_signal_connect_object (priv->toplevel_manager, + "toplevel-added", + G_CALLBACK (on_toplevel_added), + self, + G_CONNECT_SWAPPED); + + /* Screen saver manager needs lock screen manager */ + priv->screen_saver_manager = phosh_screen_saver_manager_new (priv->lockscreen_manager); + g_signal_connect_swapped (priv->screen_saver_manager, + "pb-long-press", + G_CALLBACK (on_pb_long_press), + self); + + priv->notify_manager = phosh_notify_manager_get_default (); + g_signal_connect_object (priv->notify_manager, + "new-notification", + G_CALLBACK (on_new_notification), + self, + G_CONNECT_SWAPPED); + g_signal_connect_object (priv->notify_manager, + "notification-activated", + G_CALLBACK (on_notification_activated), + self, + G_CONNECT_SWAPPED); + + phosh_shell_get_location_manager (self); + if (priv->sensor_proxy_manager) { + priv->proximity = phosh_proximity_new (priv->sensor_proxy_manager, + priv->calls_manager); + phosh_monitor_manager_set_sensor_proxy_manager (priv->monitor_manager, + priv->sensor_proxy_manager); + g_signal_connect_swapped (priv->proximity, "notify::fader", + G_CALLBACK (on_proximity_fader_changed), self); + } + + priv->mount_manager = phosh_mount_manager_new (); + priv->gtk_mount_manager = phosh_gtk_mount_manager_new (); + + phosh_session_manager_register (priv->session_manager, + PHOSH_APP_ID, + g_getenv ("DESKTOP_AUTOSTART_ID")); + g_unsetenv ("DESKTOP_AUTOSTART_ID"); + + priv->gnome_shell_manager = phosh_gnome_shell_manager_get_default (); + priv->screenshot_manager = phosh_screenshot_manager_new (); + priv->run_command_manager = phosh_run_command_manager_new (); + priv->network_auth_manager = phosh_network_auth_manager_new (); + priv->portal_access_manager = phosh_portal_access_manager_new (); + priv->suspend_manager = phosh_suspend_manager_new (); + priv->emergency_calls_manager = phosh_emergency_calls_manager_new (); + priv->power_menu_manager = phosh_power_menu_manager_new (); + priv->cell_broadcast_manager = phosh_cell_broadcast_manager_new (); + + setup_primary_monitor_signal_handlers (self); + /* Setup event hooks late so state changes in UI files don't trigger feedback */ + phosh_feedback_manager_setup_event_hooks (priv->feedback_manager); + + /* Export the debug interface late so everything is up when the name appears */ + phosh_debug_control_set_exported (priv->debug_control, TRUE); + + /* Delay signaling to the compositor a bit so that idle handlers get a chance to run and + the user can unlock right away. Ideally we'd not need this */ + priv->startup_finished_id = g_timeout_add_seconds_once (1, on_startup_finished, self); + g_source_set_name_by_id (priv->startup_finished_id, "[PhoshShell] startup finished"); + + priv->startup_finished = TRUE; + g_signal_emit (self, signals[READY], 0); + + return FALSE; +} + + +/* Load all types that might be used in UI files */ +static void +type_setup (void) +{ + g_type_ensure (PHOSH_TYPE_BATTERY_INFO); + g_type_ensure (PHOSH_TYPE_BT_INFO); + g_type_ensure (PHOSH_TYPE_CONNECTIVITY_INFO); + g_type_ensure (PHOSH_TYPE_DOCKED_INFO); + g_type_ensure (PHOSH_TYPE_FEEDBACK_INFO); + g_type_ensure (PHOSH_TYPE_HKS_INFO); + g_type_ensure (PHOSH_TYPE_LOCATION_INFO); + g_type_ensure (PHOSH_TYPE_DEFAULT_MEDIA_PLAYER); + g_type_ensure (PHOSH_TYPE_PASSWORD_ENTRY); + g_type_ensure (PHOSH_TYPE_QUICK_SETTING); + g_type_ensure (PHOSH_TYPE_REVEALER); + g_type_ensure (PHOSH_TYPE_ROTATE_INFO); + g_type_ensure (PHOSH_TYPE_SETTINGS); + g_type_ensure (PHOSH_TYPE_SYSTEM_MODAL); + g_type_ensure (PHOSH_TYPE_SYSTEM_MODAL_DIALOG); + g_type_ensure (PHOSH_TYPE_TORCH_INFO); + g_type_ensure (PHOSH_TYPE_VPN_INFO); + g_type_ensure (PHOSH_TYPE_WIFI_INFO); + g_type_ensure (PHOSH_TYPE_WWAN_INFO); +} + + +static void +phosh_shell_set_builtin_monitor (PhoshShell *self, PhoshMonitor *monitor) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor) || monitor == NULL); + priv = phosh_shell_get_instance_private (self); + + if (priv->builtin_monitor == monitor) + return; + + if (priv->builtin_monitor) { + g_clear_object (&priv->builtin_monitor); + + if (priv->rotation_manager) + phosh_rotation_manager_set_monitor (priv->rotation_manager, NULL); + } + + g_debug ("New builtin monitor is %s", monitor ? monitor->name : "(none)"); + g_set_object (&priv->builtin_monitor, monitor); + + if (monitor) { + if (priv->rotation_manager) + phosh_rotation_manager_set_monitor (priv->rotation_manager, monitor); + } + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUILTIN_MONITOR]); +} + + +/* Find a new builtin monitor that differs from old, otherwise NULL */ +static PhoshMonitor * +find_new_builtin_monitor (PhoshShell *self, PhoshMonitor *old) +{ + PhoshShellPrivate *priv; + PhoshMonitor *monitor = NULL; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (priv->monitor_manager); i++) { + PhoshMonitor *tmp = phosh_monitor_manager_get_monitor (priv->monitor_manager, i); + if (phosh_monitor_is_builtin (tmp) && tmp != old) { + monitor = tmp; + break; + } + } + + return monitor; +} + + +static void +on_monitor_added (PhoshShell *self, PhoshMonitor *monitor) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + priv = phosh_shell_get_instance_private (self); + + g_debug ("Monitor %p (%s) added", monitor, monitor->name); + + /* Set built-in monitor if not set already */ + if (!priv->builtin_monitor && phosh_monitor_is_builtin (monitor)) + phosh_shell_set_builtin_monitor (self, monitor); + + /* + * on_monitor_added() gets connected in phosh_shell_constructed() but + * we can't use phosh_shell_set_primary_monitor() yet since the + * shell object is not yet up and we can't move panels, etc. so + * ignore that case. This is not a problem since phosh_shell_constructed() + * sets the primary monitor explicitly. + */ + if (!priv->startup_finished) + return; + + /* Set primary monitor if unset */ + if (priv->primary_monitor == NULL) + phosh_shell_set_primary_monitor (self, monitor); +} + + +static void +on_monitor_removed (PhoshShell *self, PhoshMonitor *monitor) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + g_return_if_fail (PHOSH_IS_MONITOR (monitor)); + priv = phosh_shell_get_instance_private (self); + + if (priv->builtin_monitor == monitor) { + PhoshMonitor *new_builtin; + + g_debug ("Builtin monitor %p (%s) removed", monitor, monitor->name); + + new_builtin = find_new_builtin_monitor (self, monitor); + phosh_shell_set_builtin_monitor (self, new_builtin); + } + + if (priv->primary_monitor == monitor) { + g_debug ("Primary monitor %p (%s) removed", monitor, monitor->name); + + /* Prefer built in monitor when primary is gone... */ + if (priv->builtin_monitor) { + phosh_shell_set_primary_monitor (self, priv->builtin_monitor); + return; + } + + /* ...just pick the first one available otherwise */ + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (priv->monitor_manager); i++) { + PhoshMonitor *new_primary = phosh_monitor_manager_get_monitor (priv->monitor_manager, i); + if (new_primary != monitor) { + phosh_shell_set_primary_monitor (self, new_primary); + return; + } + } + + /* We did not find another monitor so all monitors are gone */ + phosh_shell_set_primary_monitor (self, NULL); + } +} + + +static void +on_keyboard_events_pressed (PhoshShell *self, const char *combo) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!phosh_calls_manager_has_incoming_call (priv->calls_manager)) + return; + + if (!g_settings_get_boolean (priv->settings, "quick-silent")) + return; + + if (g_strcmp0 (combo, "XF86AudioLowerVolume")) + return; + + g_debug ("Vol down pressed, silencing device"); + phosh_feedback_manager_set_profile (priv->feedback_manager, "silent"); +} + + +static void +phosh_shell_constructed (GObject *object) +{ + PhoshShell *self = PHOSH_SHELL (object); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + g_autoptr (GError) err = NULL; + guint id; + + G_OBJECT_CLASS (phosh_shell_parent_class)->constructed (object); + + priv->monitor_manager = phosh_monitor_manager_new (NULL); + g_signal_connect_swapped (priv->monitor_manager, + "monitor-added", + G_CALLBACK (on_monitor_added), + self); + g_signal_connect_swapped (priv->monitor_manager, + "monitor-removed", + G_CALLBACK (on_monitor_removed), + self); + + /* Make sure all outputs are up to date */ + phosh_wayland_roundtrip (phosh_wayland_get_default ()); + + if (phosh_monitor_manager_get_num_monitors (priv->monitor_manager)) { + PhoshMonitor *monitor = find_new_builtin_monitor (self, NULL); + + /* Setup builtin monitor if not set via 'monitor-added' */ + if (!priv->builtin_monitor && monitor) { + phosh_shell_set_builtin_monitor (self, monitor); + g_debug ("Builtin monitor %p, configured: %d", + priv->builtin_monitor, + phosh_monitor_is_configured (priv->builtin_monitor)); + } + + /* Setup primary monitor, prefer builtin */ + /* Can't invoke phosh_shell_set_primary_monitor () since this involves + updating the panels as well and we need to init more of the shell first */ + if (priv->builtin_monitor) + priv->primary_monitor = g_object_ref (priv->builtin_monitor); + else + priv->primary_monitor = g_object_ref (phosh_monitor_manager_get_monitor (priv->monitor_manager, 0)); + } else { + g_error ("Need at least one monitor"); + } + + priv->calls_manager = phosh_calls_manager_new (); + priv->launcher_entry_manager = phosh_launcher_entry_manager_new (); + + priv->lockscreen_manager = phosh_lockscreen_manager_new (priv->calls_manager); + g_object_bind_property (priv->lockscreen_manager, "locked", + self, "locked", + G_BINDING_BIDIRECTIONAL | G_BINDING_SYNC_CREATE); + + priv->idle_manager = phosh_idle_manager_get_default (); + + priv->faders = g_ptr_array_new_with_free_func ((GDestroyNotify) (gtk_widget_destroy)); + + phosh_system_prompter_register (); + priv->polkit_auth_agent = phosh_polkit_auth_agent_new (); + + priv->feedback_manager = phosh_feedback_manager_new (); + priv->keyboard_events = phosh_keyboard_events_new (&err); + if (priv->keyboard_events) { + g_signal_connect_swapped (priv->keyboard_events, + "pressed", + G_CALLBACK (on_keyboard_events_pressed), + self); + } else { + g_warning ("Failed to initialize keyboard events: %s", err->message); + } + + id = g_idle_add ((GSourceFunc) setup_idle_cb, self); + g_source_set_name_by_id (id, "[PhoshShell] idle"); +} + +/* {{{ Action Map/Group */ + +static char ** +phosh_shell_list_actions (GActionGroup *group) +{ + PhoshShell *self = PHOSH_SHELL (group); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + /* may be NULL after dispose has run */ + if (!priv->action_map) + return g_new0 (char *, 0 + 1); + + return g_action_group_list_actions (G_ACTION_GROUP (priv->action_map)); +} + +static gboolean +phosh_shell_query_action (GActionGroup *group, + const char *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + PhoshShell *self = PHOSH_SHELL (group); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return FALSE; + + return g_action_group_query_action (G_ACTION_GROUP (priv->action_map), + action_name, + enabled, + parameter_type, + state_type, + state_hint, + state); +} + + +static void +_phosh_shell_activate_action (GActionGroup *group, + const char *action_name, + GVariant *parameter) +{ + PhoshShell *self = PHOSH_SHELL (group); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return; + + g_action_group_activate_action (G_ACTION_GROUP (priv->action_map), action_name, parameter); +} + + +static void +phosh_shell_change_action_state (GActionGroup *group, + const char *action_name, + GVariant *state) +{ + PhoshShell *self = PHOSH_SHELL (group); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return; + + g_action_group_change_action_state (G_ACTION_GROUP (priv->action_map), action_name, state); +} + + +static void +phosh_shell_action_group_iface_init (GActionGroupInterface *iface) +{ + iface->list_actions = phosh_shell_list_actions; + iface->query_action = phosh_shell_query_action; + iface->activate_action = _phosh_shell_activate_action; + iface->change_action_state = phosh_shell_change_action_state; +} + + +static GAction * +phosh_shell_lookup_action (GActionMap *action_map, const char *action_name) +{ + PhoshShell *self = PHOSH_SHELL (action_map); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return NULL; + + return g_action_map_lookup_action (G_ACTION_MAP (priv->action_map), action_name); +} + +static void +phosh_shell_add_action (GActionMap *action_map, GAction *action) +{ + PhoshShell *self = PHOSH_SHELL (action_map); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return; + + g_action_map_add_action (G_ACTION_MAP (priv->action_map), action); +} + +static void +phosh_shell_remove_action (GActionMap *action_map, const char *action_name) +{ + PhoshShell *self = PHOSH_SHELL (action_map); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + if (!priv->action_map) + return; + + g_action_map_remove_action (G_ACTION_MAP (priv->action_map), action_name); +} + + +static void +phosh_shell_action_map_iface_init (GActionMapInterface *iface) +{ + iface->lookup_action = phosh_shell_lookup_action; + iface->add_action = phosh_shell_add_action; + iface->remove_action = phosh_shell_remove_action; +} + + +static GType +get_lockscreen_type (PhoshShell *self) +{ + return PHOSH_TYPE_LOCKSCREEN; +} + +/* }}} */ +/* {{{ GObject init */ + +static void +phosh_shell_class_init (PhoshShellClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->constructed = phosh_shell_constructed; + object_class->dispose = phosh_shell_dispose; + object_class->finalize = phosh_shell_finalize; + + object_class->set_property = phosh_shell_set_property; + object_class->get_property = phosh_shell_get_property; + + klass->get_lockscreen_type = get_lockscreen_type; + + type_setup (); + + /** + * PhoshShell:locked: + * + * Whether the screen is currently locked. This mirrors the property + * from #PhoshLockscreenManager for easier access. + */ + props[PROP_LOCKED] = + g_param_spec_boolean ("locked", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshShell:docked: + * + * Whether the device is currently docked. This mirrors the property + * from #PhoshDockedManager for easier access. + */ + props[PROP_DOCKED] = + g_param_spec_boolean ("docked", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + /** + * PhoshShell:builtin-monitor: + * + * The built in monitor. This is a hardware property and hence can + * only be read. It can be %NULL when not present or disabled. + * + * Since: 0.10.1 + */ + props[PROP_BUILTIN_MONITOR] = + g_param_spec_object ("builtin-monitor", "", "", + PHOSH_TYPE_MONITOR, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshShell:primary-monitor: + * + * The primary monitor that has the panels, lock screen etc. + * + * Since: 0.0.2 + */ + props[PROP_PRIMARY_MONITOR] = + g_param_spec_object ("primary-monitor", "", "", + PHOSH_TYPE_MONITOR, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshShell:shell-state: + * + * The state of the shell (locked, modal dialog shown, …) + * + * Since: 0.10.0 + */ + props[PROP_SHELL_STATE] = + g_param_spec_flags ("shell-state", "", "", + PHOSH_TYPE_SHELL_STATE_FLAGS, + PHOSH_STATE_NONE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshShell:overview-visible: + * + * Whether to display the `PhoshHome` (overview and home bar) + */ + props[PROP_OVERVIEW_VISIBLE] = + g_param_spec_boolean ("overview-visible", "", "", + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + /** + * PhoshShell:log-domains + * + * The current log domains + */ + props[PROP_LOG_DOMAINS] = + g_param_spec_boxed ("log-domains", "", "", + G_TYPE_STRV, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshShell::ready: + * @self: The shell object + * + * The ready signal is emitted once when the shell finished starting + * up. + * + * Since: 0.11.0 + */ + signals[READY] = g_signal_new ("ready", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + + +static GDebugKey debug_keys[] = +{ + { .key = "always-splash", + .value = PHOSH_SHELL_DEBUG_FLAG_ALWAYS_SPLASH, + }, + { .key = "fake-builtin", + .value = PHOSH_SHELL_DEBUG_FLAG_FAKE_BUILTIN, + }, + { .key = "backlight-non-linear", + .value = PHOSH_SHELL_DEBUG_BACKLIGHT_NON_LINEAR, + }, +}; + + +static void +phosh_shell_init (PhoshShell *self) +{ + const char *messages_debug; + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + messages_debug = g_getenv ("G_MESSAGES_DEBUG"); + if (messages_debug) + priv->log_domains = g_strsplit (messages_debug, " ", -1); + + cui_init (TRUE); + gtk_icon_theme_add_resource_path (gtk_icon_theme_get_default (), "/mobi/phosh/icons"); + + priv->overview_visible = TRUE; + + g_io_extension_point_register (PHOSH_EXTENSION_POINT_LOCKSCREEN_WIDGET); + g_io_extension_point_register (PHOSH_EXTENSION_POINT_QUICK_SETTING_WIDGET); + + debug_flags = g_parse_debug_string (g_getenv ("PHOSH_DEBUG"), + debug_keys, + G_N_ELEMENTS (debug_keys)); + + priv->style_manager = phosh_style_manager_new (); + priv->shell_state = PHOSH_STATE_SETTINGS; + priv->action_map = g_simple_action_group_new (); + priv->settings = g_settings_new ("sm.puri.phosh"); + + /* We bind this early since a wl_display_roundtrip () would make us miss + existing toplevels */ + priv->toplevel_manager = phosh_toplevel_manager_new (); + priv->udev_manager = phosh_udev_manager_get_default (); +} + +/* }}} */ + +PhoshShell * +phosh_shell_new (void) +{ + return g_object_new (PHOSH_TYPE_SHELL, NULL); +} + +static gboolean +select_fallback_monitor (gpointer data) +{ + PhoshShell *self = PHOSH_SHELL (data); + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MONITOR_MANAGER (priv->monitor_manager), FALSE); + phosh_monitor_manager_enable_fallback (priv->monitor_manager); + + return G_SOURCE_REMOVE; +} + +/* {{{ Public functions */ + +void +phosh_shell_set_primary_monitor (PhoshShell *self, PhoshMonitor *monitor) +{ + PhoshShellPrivate *priv; + PhoshMonitor *m = NULL; + gboolean needs_notify = FALSE; + + g_return_if_fail (PHOSH_IS_MONITOR (monitor) || monitor == NULL); + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + if (monitor == priv->primary_monitor) + return; + + if (priv->primary_monitor) { + g_signal_handlers_disconnect_by_func (priv->primary_monitor, + G_CALLBACK (on_primary_monitor_configured), + self); + } + if (priv->builtin_monitor) { + g_signal_handlers_disconnect_by_func (priv->builtin_monitor, + G_CALLBACK (on_primary_monitor_power_mode_changed), + self); + } + + if (monitor != NULL) { + /* Make sure the new monitor exists */ + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (priv->monitor_manager); i++) { + m = phosh_monitor_manager_get_monitor (priv->monitor_manager, i); + if (monitor == m) + break; + } + g_return_if_fail (monitor == m); + } + + needs_notify = priv->primary_monitor == NULL; + g_set_object (&priv->primary_monitor, monitor); + g_debug ("New primary monitor is %s", monitor ? monitor->name : "(none)"); + + /* Move panels to the new monitor by recreating the layer-shell surfaces */ + panels_dispose (self); + if (monitor) + panels_create (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRIMARY_MONITOR]); + + setup_primary_monitor_signal_handlers (self); + + /* All monitors gone or disabled. See if monitor-manager finds a + * fallback to enable. Do that in an idle callback so GTK can process + * pending wayland events for the gone output */ + if (monitor == NULL) { + guint id; + /* No monitor - we're not useful atm */ + notify_compositor_up_state (self, PHOSH_PRIVATE_SHELL_STATE_UNKNOWN); + id = g_idle_add (select_fallback_monitor, self); + g_source_set_name_by_id (id, "[PhoshShell] select fallback monitor"); + } else { + if (needs_notify) + notify_compositor_up_state (self, PHOSH_PRIVATE_SHELL_STATE_UP); + } +} + +/** + * phosh_shell_get_builtin_monitor: + * @self: The shell + * + * Returns:(transfer none)(nullable): the built in monitor or %NULL if there is no built in monitor + */ +PhoshMonitor * +phosh_shell_get_builtin_monitor (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_MONITOR (priv->builtin_monitor) || priv->builtin_monitor == NULL, + NULL); + + return priv->builtin_monitor; +} + +/** + * phosh_shell_get_primary_monitor: + * @self: The shell + * + * Returns:(transfer none)(nullable): the primary monitor or %NULL if there currently are no outputs + */ +PhoshMonitor * +phosh_shell_get_primary_monitor (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + return priv->primary_monitor; +} + +/* Manager getters */ + +/** + * phosh_shell_get_ambient: + * @self: The shell singleton + * + * Get the ambient light manager + * + * Returns: (nullable)(transfer none): The ambient light manager. + */ +PhoshAmbient * +phosh_shell_get_ambient (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_AMBIENT (priv->ambient) || priv->ambient == NULL, NULL); + + return priv->ambient; +} + +/** + * phosh_shell_get_app_tracker: + * @self: The shell singleton + * + * Get the app tracker + * + * Returns: (transfer none): The app tracker + */ +PhoshAppTracker * +phosh_shell_get_app_tracker (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_APP_TRACKER (priv->app_tracker), NULL); + + return priv->app_tracker; +} + +/** + * phosh_shell_get_background_manager: + * @self: The shell singleton + * + * Get the background manager + * + * Returns: (transfer none): The background manager + */ +PhoshBackgroundManager * +phosh_shell_get_background_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_BACKGROUND_MANAGER (priv->background_manager), NULL); + + return priv->background_manager; +} + +/** + * phosh_shell_get_calls_manager: + * @self: The shell singleton + * + * Get the calls manager + * + * Returns: (transfer none): The calls manager + */ +PhoshCallsManager * +phosh_shell_get_calls_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_CALLS_MANAGER (priv->calls_manager), NULL); + + return priv->calls_manager; +} + +/** + * phosh_shell_get_connectivity_manager: + * @self: The shell singleton + * + * Get the connectivity manager + * + * Returns: (transfer none): The connectivity manager + */ +PhoshConnectivityManager * +phosh_shell_get_connectivity_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_CONNECTIVITY_MANAGER (priv->connectivity_manager), NULL); + return priv->connectivity_manager; +} + +/** + * phosh_shell_get_emergency_calls_manager: + * @self: The shell singleton + * + * Get the emergency calls manager + * + * Returns: (transfer none): The emergency calls manager + */ +PhoshEmergencyCallsManager* +phosh_shell_get_emergency_calls_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_EMERGENCY_CALLS_MANAGER (priv->emergency_calls_manager), NULL); + + return priv->emergency_calls_manager; +} + +/** + * phosh_shell_get_feedback_manager: + * @self: The shell singleton + * + * Get the feedback manager + * + * Returns: (transfer none): The feedback manager + */ +PhoshFeedbackManager * +phosh_shell_get_feedback_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_FEEDBACK_MANAGER (priv->feedback_manager), NULL); + + return priv->feedback_manager; +} + +/** + * phosh_shell_get_gtk_mount_manager: + * @self: The shell singleton + * + * Get the GTK mount manager + * + * Returns: (transfer none): The GTK mount manager + */ +PhoshGtkMountManager * +phosh_shell_get_gtk_mount_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_GTK_MOUNT_MANAGER (priv->gtk_mount_manager), NULL); + + return priv->gtk_mount_manager; +} + +/** + * phosh_shell_get_launcher_entry_manager: + * @self: The shell singleton + * + * Get the launcher entry manager + * + * Returns: (transfer none): The launcher entry manager + */ +PhoshLauncherEntryManager * +phosh_shell_get_launcher_entry_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_LAUNCHER_ENTRY_MANAGER (priv->launcher_entry_manager), NULL); + return priv->launcher_entry_manager; +} + +/** + * phosh_shell_get_layout_manager: + * @self: The shell singleton + * + * Get the layout manager + * + * Returns: (transfer none): The layout manager + */ +PhoshLayoutManager * +phosh_shell_get_layout_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_LAYOUT_MANAGER (priv->layout_manager), NULL); + return priv->layout_manager; +} + +/** + * phosh_shell_get_lockscreen_manager: + * @self: The shell singleton + * + * Get the lockscreen manager + * + * Returns: (transfer none): The lockscreen manager + */ +PhoshLockscreenManager * +phosh_shell_get_lockscreen_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_LOCKSCREEN_MANAGER (priv->lockscreen_manager), NULL); + return priv->lockscreen_manager; +} + +/** + * phosh_shell_get_mode_manager: + * @self: The shell singleton + * + * Get the mode manager + * + * Returns: (transfer none): The mode manager + */ +PhoshModeManager * +phosh_shell_get_mode_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MODE_MANAGER (priv->mode_manager), NULL); + return priv->mode_manager; +} + +/** + * phosh_shell_get_style_manager: + * @self: The shell singleton + * + * Get the style manager + * + * Returns: (transfer none): The style manager + */ +PhoshStyleManager * +phosh_shell_get_style_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_assert (PHOSH_IS_STYLE_MANAGER (priv->style_manager)); + return priv->style_manager; +} + +/** + * phosh_shell_get_monitor_manager: + * @self: The shell singleton + * + * Get the monitor manager + * + * Returns: (transfer none): The monitor manager + */ +PhoshMonitorManager * +phosh_shell_get_monitor_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_MONITOR_MANAGER (priv->monitor_manager), NULL); + return priv->monitor_manager; +} + +/** + * phosh_shell_get_toplevel_manager: + * @self: The shell singleton + * + * Get the toplevel manager + * + * Returns: (transfer none): The toplevel manager + */ +PhoshToplevelManager * +phosh_shell_get_toplevel_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (priv->toplevel_manager), NULL); + return priv->toplevel_manager; +} + +/** + * phosh_shell_get_screen_saver_manager: + * @self: The shell singleton + * + * Get the screensaver manager + * + * Returns: (transfer none): The screensaver manager + */ +PhoshScreenSaverManager * +phosh_shell_get_screen_saver_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_SCREEN_SAVER_MANAGER (self), NULL); + return priv->screen_saver_manager; +} + +/** + * phosh_shell_get_screenshot_manager: + * @self: The shell singleton + * + * Get the screenshot manager + * + * Returns: (transfer none): The screenshot manager + */ +PhoshScreenshotManager * +phosh_shell_get_screenshot_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_SCREENSHOT_MANAGER (priv->screenshot_manager), NULL); + return priv->screenshot_manager; +} + +/** + * phosh_shell_get_session_manager: + * @self: The shell singleton + * + * Get the session manager + * + * Returns: (transfer none): The session manager + */ +PhoshSessionManager * +phosh_shell_get_session_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_SESSION_MANAGER (priv->session_manager), NULL); + + return priv->session_manager; +} + +/** + * phosh_shell_get_splash_manager: + * @self: The shell singleton + * + * Get the splash manager + * + * Returns: (transfer none): The splash manager + */ +PhoshSplashManager * +phosh_shell_get_splash_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_SPLASH_MANAGER (priv->splash_manager), NULL); + + return priv->splash_manager; +} + +/** + * phosh_shell_get_wifi_manager: + * @self: The shell singleton + * + * Get the Wifi manager + * + * Returns: (transfer none): The Wifi manager + */ +PhoshWifiManager * +phosh_shell_get_wifi_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_WIFI_MANAGER (priv->wifi_manager), NULL); + return priv->wifi_manager; +} + +/* Manager getters that create them as needed */ + +/** + * phosh_shell_get_brightness_manager: + * @self: The shell singleton + * + * Get the brightness manager + * + * Returns: (transfer none): The brightness manager + */ +PhoshBrightnessManager * +phosh_shell_get_brightness_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + + if (!priv->brightness_manager) + priv->brightness_manager = phosh_brightness_manager_new (); + + g_return_val_if_fail (PHOSH_IS_BRIGHTNESS_MANAGER (priv->brightness_manager), NULL); + + return priv->brightness_manager; +} + +/** + * phosh_shell_get_battery_manager: + * @self: The shell singleton + * + * Get the battery manager + * + * Returns: (transfer none): The battery manager + */ +PhoshBatteryManager * +phosh_shell_get_battery_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->battery_manager) + priv->battery_manager = phosh_battery_manager_new (); + + g_return_val_if_fail (PHOSH_IS_BATTERY_MANAGER (priv->battery_manager), NULL); + return priv->battery_manager; +} + +/** + * phosh_shell_get_bt_manager: + * @self: The shell singleton + * + * Get the bluetooth manager + * + * Returns: (transfer none): The bluetooth manager + */ +PhoshBtManager * +phosh_shell_get_bt_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->bt_manager) + priv->bt_manager = phosh_bt_manager_new (); + + g_return_val_if_fail (PHOSH_IS_BT_MANAGER (priv->bt_manager), NULL); + return priv->bt_manager; +} + +/** + * phosh_shell_get_docked_manager: + * @self: The shell singleton + * + * Get the docked manager + * + * Returns: (transfer none): The docked manager + */ +PhoshDockedManager * +phosh_shell_get_docked_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->docked_manager) { + priv->docked_manager = phosh_docked_manager_new (priv->mode_manager); + g_object_bind_property (priv->docked_manager, + "enabled", + self, + "docked", + G_BINDING_SYNC_CREATE); + } + + g_return_val_if_fail (PHOSH_IS_DOCKED_MANAGER (priv->docked_manager), NULL); + return priv->docked_manager; +} + +/** + * phosh_shell_get_hks_manager: + * @self: The shell singleton + * + * Get the hardware killswitch manager + * + * Returns: (transfer none): The hardware killswitch manager + */ +PhoshHksManager * +phosh_shell_get_hks_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->hks_manager) + priv->hks_manager = phosh_hks_manager_new (); + + g_return_val_if_fail (PHOSH_IS_HKS_MANAGER (priv->hks_manager), NULL); + return priv->hks_manager; +} + +/** + * phosh_shell_get_location_manager: + * @self: The shell singleton + * + * Get the location manager + * + * Returns: (transfer none): The location manager + */ +PhoshLocationManager * +phosh_shell_get_location_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->location_manager) + priv->location_manager = phosh_location_manager_new (); + + g_return_val_if_fail (PHOSH_IS_LOCATION_MANAGER (priv->location_manager), NULL); + return priv->location_manager; +} + +/** + * phosh_shell_get_mpris_manager: + * @self: The shell singleton + * + * Get the MPRIS manager + * + * Returns: (transfer none): The MPRIS manager + */ +PhoshMprisManager * +phosh_shell_get_mpris_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->mpris_manager) + priv->mpris_manager = phosh_mpris_manager_new (); + + g_return_val_if_fail (PHOSH_IS_MPRIS_MANAGER (priv->mpris_manager), NULL); + return priv->mpris_manager; +} + +/** + * phosh_shell_get_osk_manager: + * @self: The shell singleton + * + * Get the onscreen keyboard manager + * + * Returns: (transfer none): The onscreen keyboard manager + */ +PhoshOskManager * +phosh_shell_get_osk_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->osk_manager) + priv->osk_manager = phosh_osk_manager_new (); + + g_return_val_if_fail (PHOSH_IS_OSK_MANAGER (priv->osk_manager), NULL); + return priv->osk_manager; +} + +/** + * phosh_shell_get_rotation_manager: + * @self: The shell singleton + * + * Get the rotation manager + * + * Returns: (transfer none): The rotation manager + */ +PhoshRotationManager * +phosh_shell_get_rotation_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->rotation_manager) { + priv->rotation_manager = phosh_rotation_manager_new (priv->sensor_proxy_manager, + priv->lockscreen_manager, + priv->builtin_monitor); + /* + * Make sure rotation works even if the primary monitor has already appeared + * when we create the rotation manager. + */ + phosh_rotation_manager_set_monitor (priv->rotation_manager, priv->primary_monitor); + } + + g_return_val_if_fail (PHOSH_IS_ROTATION_MANAGER (priv->rotation_manager), NULL); + + return priv->rotation_manager; +} + +/** + * phosh_shell_get_torch_manager: + * @self: The shell singleton + * + * Get the torch manager + * + * Returns: (transfer none): The torch manager + */ +PhoshTorchManager * +phosh_shell_get_torch_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->torch_manager) + priv->torch_manager = phosh_torch_manager_new (); + + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (priv->torch_manager), NULL); + return priv->torch_manager; +} + +/** + * phosh_shell_get_vpn_manager: + * @self: The shell singleton + * + * Get the VPN manager + * + * Returns: (transfer none): The VPN manager + */ +PhoshVpnManager * +phosh_shell_get_vpn_manager (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->vpn_manager) + priv->vpn_manager = phosh_vpn_manager_new (); + + g_return_val_if_fail (PHOSH_IS_VPN_MANAGER (priv->vpn_manager), NULL); + return priv->vpn_manager; +} + +/** + * phosh_shell_get_wwan: + * @self: The shell singleton + * + * Get the WWAN manager + * + * Returns: (transfer none): The WWAN manager + */ +PhoshWWan * +phosh_shell_get_wwan (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + if (!priv->wwan) { + PhoshWWanBackend backend = g_settings_get_enum (priv->settings, WWAN_BACKEND_KEY); + + switch (backend) { + default: + case PHOSH_WWAN_BACKEND_MM: + priv->wwan = PHOSH_WWAN (phosh_wwan_mm_new()); + break; + case PHOSH_WWAN_BACKEND_OFONO: + priv->wwan = PHOSH_WWAN (phosh_wwan_ofono_new()); + break; + } + } + + g_return_val_if_fail (PHOSH_IS_WWAN (priv->wwan), NULL); + return priv->wwan; +} + +/** + * phosh_shell_get_usable_area: + * @self: The shell + * @x:(out)(nullable): The x coordinate where client usable area starts + * @y:(out)(nullable): The y coordinate where client usable area starts + * @width:(out)(nullable): The width of the client usable area + * @height:(out)(nullable): The height of the client usable area + * + * Gives the usable area in pixels usable by a client on the primary + * display. + */ +void +phosh_shell_get_usable_area (PhoshShell *self, int *x, int *y, int *width, int *height) +{ + PhoshMonitor *monitor; + PhoshMonitorMode *mode; + int w, h; + float scale; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + + monitor = phosh_shell_get_primary_monitor (self); + g_return_if_fail (monitor); + mode = phosh_monitor_get_current_mode (monitor); + g_return_if_fail (mode != NULL); + + scale = MAX (1.0, phosh_monitor_get_fractional_scale (monitor)); + + g_debug ("Primary monitor %p scale is %f, mode: %dx%d, transform is %d", + monitor, + scale, + mode->width, + mode->height, + monitor->transform); + + switch (phosh_monitor_get_transform (monitor)) { + case PHOSH_MONITOR_TRANSFORM_NORMAL: + case PHOSH_MONITOR_TRANSFORM_180: + case PHOSH_MONITOR_TRANSFORM_FLIPPED: + case PHOSH_MONITOR_TRANSFORM_FLIPPED_180: + w = mode->width / scale; + h = mode->height / scale - PHOSH_TOP_BAR_HEIGHT - PHOSH_HOME_BAR_HEIGHT; + break; + default: + w = mode->height / scale; + h = mode->width / scale - PHOSH_TOP_BAR_HEIGHT - PHOSH_HOME_BAR_HEIGHT; + break; + } + + if (x) + *x = 0; + if (y) + *y = PHOSH_TOP_BAR_HEIGHT; + if (width) + *width = w; + if (height) + *height = h; +} + +/** + * phosh_shell_get_area: + * @self: The shell singleton + * @width:(out)(nullable): The available width + * @height:(out)(nullable): The available height + * + * Gives the currently available screen area on the primary display. + */ +void +phosh_shell_get_area (PhoshShell *self, int *width, int *height) +{ + int w, h; + + phosh_shell_get_usable_area (self, NULL, NULL, &w, &h); + + if (width) + *width = w; + + if (height) + *height = h + PHOSH_TOP_BAR_HEIGHT + PHOSH_HOME_BAR_HEIGHT; +} + +static PhoshShell *instance; + +/** + * phosh_shell_set_default: + * @self: The shell to use + * + * Set the PhoshShell singleton that is returned by `phosh_shell_get_default()` + */ +void +phosh_shell_set_default (PhoshShell *self) +{ + g_return_if_fail (PHOSH_IS_SHELL (self)); + + g_clear_object (&instance); + + instance = self; + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); +} + +/** + * phosh_shell_get_default: + * + * Get the shell singleton + * + * Returns:(transfer none): The shell singleton + */ +PhoshShell * +phosh_shell_get_default (void) +{ + if (!instance) + g_error ("Shell singleton not set"); + return instance; +} + +void +phosh_shell_fade_out (PhoshShell *self, guint timeout) +{ + PhoshShellPrivate *priv; + PhoshMonitorManager *monitor_manager; + + g_debug ("Fading out..."); + g_return_if_fail (PHOSH_IS_SHELL (self)); + monitor_manager = phosh_shell_get_monitor_manager (self); + g_return_if_fail (PHOSH_IS_MONITOR_MANAGER (monitor_manager)); + priv = phosh_shell_get_instance_private (self); + + for (int i = 0; i < phosh_monitor_manager_get_num_monitors (monitor_manager); i++) { + PhoshFader *fader; + PhoshMonitor *monitor = phosh_monitor_manager_get_monitor (monitor_manager, i); + + fader = phosh_fader_new (monitor); + g_ptr_array_add (priv->faders, fader); + gtk_widget_set_visible (GTK_WIDGET (fader), TRUE); + if (timeout > 0) { + guint id; + + id = g_timeout_add_seconds (timeout, (GSourceFunc) on_fade_out_timeout, self); + g_source_set_name_by_id (id, "[PhoshShell] fade out"); + } + } +} + +/** + * phosh_shell_set_power_save: + * @self: The shell + * @enable: Whether power save mode should be enabled + * + * Enter power saving mode. This currently blanks all monitors. + */ +void +phosh_shell_enable_power_save (PhoshShell *self, gboolean enable) +{ + PhoshShellPrivate *priv; + PhoshMonitorPowerSaveMode ps_mode; + + g_debug ("Entering power save mode: %d", enable); + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + ps_mode = enable ? PHOSH_MONITOR_POWER_SAVE_MODE_OFF : PHOSH_MONITOR_POWER_SAVE_MODE_ON; + phosh_monitor_manager_set_power_save_mode (priv->monitor_manager, ps_mode); +} + +/** + * phosh_shell_started_by_display_manager: + * @self: The shell + * + * Returns: %TRUE if we were started from a display manager. %FALSE otherwise. + */ +gboolean +phosh_shell_started_by_display_manager (PhoshShell *self) +{ + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + + if (!g_strcmp0 (g_getenv ("GDMSESSION"), "phosh")) + return TRUE; + + return FALSE; +} + +/** + * phosh_shell_is_startup_finished: + * @self: The shell + * + * Returns: %TRUE if the shell finished startup. %FALSE otherwise. + */ +gboolean +phosh_shell_is_startup_finished (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + priv = phosh_shell_get_instance_private (self); + + return priv->startup_finished; +} + + +void +phosh_shell_add_global_keyboard_action_entries (PhoshShell *self, + const GActionEntry *entries, + gint n_entries, + gpointer user_data) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + g_return_if_fail (priv->keyboard_events); + + g_action_map_add_action_entries (G_ACTION_MAP (priv->keyboard_events), + entries, + n_entries, + user_data); +} + + +void +phosh_shell_remove_global_keyboard_action_entries (PhoshShell *self, + GStrv action_names) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + g_return_if_fail (priv->keyboard_events); + + for (int i = 0; i < g_strv_length (action_names); i++) { + g_action_map_remove_action (G_ACTION_MAP (priv->keyboard_events), + action_names[i]); + } +} + +/** + * phosh_shell_is_session_active + * @self: The shell + * + * Whether this shell is part of the active session + */ +gboolean +phosh_shell_is_session_active (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + priv = phosh_shell_get_instance_private (self); + + return phosh_session_manager_is_active (priv->session_manager); +} + +/** + * phosh_shell_get_app_launch_context: + * @self: The shell + * + * Returns: (transfer none): an app launch context for the primary display + */ +GdkAppLaunchContext* +phosh_shell_get_app_launch_context (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), NULL); + priv = phosh_shell_get_instance_private (self); + + return gdk_display_get_app_launch_context (gtk_widget_get_display (GTK_WIDGET (priv->top_panel))); +} + +/** + * phosh_shell_get_state + * @self: The shell + * + * Returns: The current #PhoshShellStateFlags + */ +PhoshShellStateFlags +phosh_shell_get_state (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), PHOSH_STATE_NONE); + priv = phosh_shell_get_instance_private (self); + + return priv->shell_state; +} + +/** + * phosh_shell_set_state: + * @self: The shell + * @state: The #PhoshShellStateFlags to set + * @enabled: %TRUE to set a shell state, %FALSE to reset + * + * Set the shells state. + */ +void +phosh_shell_set_state (PhoshShell *self, + PhoshShellStateFlags state, + gboolean enabled) +{ + PhoshShellPrivate *priv; + PhoshShellStateFlags old_state; + g_autofree char *str_state = NULL; + g_autofree char *str_new_flags = NULL; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + old_state = priv->shell_state; + + if (enabled) + priv->shell_state = priv->shell_state | state; + else + priv->shell_state = priv->shell_state & ~state; + + if (old_state == priv->shell_state) + return; + + str_state = g_flags_to_string (PHOSH_TYPE_SHELL_STATE_FLAGS, + state); + str_new_flags = g_flags_to_string (PHOSH_TYPE_SHELL_STATE_FLAGS, + priv->shell_state); + + g_debug ("%s %s %s shells state. New state: %s", + enabled ? "Adding" : "Removing", + str_state, + enabled ? "to" : "from", str_new_flags); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHELL_STATE]); + + if (state & PHOSH_STATE_MODAL_SYSTEM_PROMPT) + update_top_level_layer (self); +} + +void +phosh_shell_lock (PhoshShell *self) +{ + g_return_if_fail (PHOSH_IS_SHELL (self)); + + phosh_shell_set_locked (self, TRUE); +} + + +void +phosh_shell_unlock (PhoshShell *self) +{ + g_return_if_fail (PHOSH_IS_SHELL (self)); + + phosh_shell_set_locked (self, FALSE); +} + +/** + * phosh_shell_get_locked: + * @self: The #PhoshShell singleton + * + * Returns: %TRUE if the shell is currently locked, otherwise %FALSE. + */ +gboolean +phosh_shell_get_locked (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + priv = phosh_shell_get_instance_private (self); + + return priv->locked; +} + +/** + * phosh_shell_set_locked: + * @self: The #PhoshShell singleton + * @locked: %TRUE to lock the shell + * + * Lock the shell. We proxy to lockscreen-manager to avoid + * that other parts of the shell need to care about this + * abstraction. + */ +void +phosh_shell_set_locked (PhoshShell *self, gboolean locked) +{ + PhoshShellPrivate *priv; + + g_return_if_fail (PHOSH_IS_SHELL (self)); + priv = phosh_shell_get_instance_private (self); + + if (locked == priv->locked) + return; + + phosh_lockscreen_manager_set_locked (priv->lockscreen_manager, locked); +} + +/** + * phosh_shell_get_show_splash: + * @self: The #PhoshShell singleton + * + * Whether splash screens should be used when apps start + * Returns: %TRUE when splash should be used, otherwise %FALSE + */ +gboolean +phosh_shell_get_show_splash (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), TRUE); + priv = phosh_shell_get_instance_private (self); + g_return_val_if_fail (PHOSH_IS_DOCKED_MANAGER (priv->docked_manager), TRUE); + + if (debug_flags & PHOSH_SHELL_DEBUG_FLAG_ALWAYS_SPLASH) + return TRUE; + + if (phosh_docked_manager_get_enabled (priv->docked_manager)) + return FALSE; + + return TRUE; +} + +/** + * phosh_shell_get_docked: + * @self: The #PhoshShell singleton + * + * Returns: %TRUE if the device is currently docked, otherwise %FALSE. + */ +gboolean +phosh_shell_get_docked (PhoshShell *self) +{ + PhoshShellPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + priv = phosh_shell_get_instance_private (self); + + return priv->docked; +} + +/** + * phosh_shell_get_blanked: + * @self: The #PhoshShell singleton + * + * Returns: %TRUE if the primary output is currently blanked + */ +gboolean +phosh_shell_get_blanked (PhoshShell *self) +{ + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + + return phosh_shell_get_state (self) & PHOSH_STATE_BLANKED; +} + +/** + * phosh_shell_activate_action: + * @self: The #PhoshShell singleton + * @action: The action name + * @parameter: The action's parameters + * + * Activates the given action. If the action is not found %FALSE is returned and a + * warning is logged. + * + * Returns: %TRUE if the action was found + */ +gboolean +phosh_shell_activate_action (PhoshShell *self, const char *action, GVariant *parameter) +{ + g_return_val_if_fail (PHOSH_IS_SHELL (self), FALSE); + g_return_val_if_fail (action, FALSE); + + if (g_action_group_has_action (G_ACTION_GROUP (self), action) == FALSE) { + g_warning ("No such action '%s' on shell object", action); + return FALSE; + } + + g_action_group_activate_action (G_ACTION_GROUP (self), action, parameter); + return TRUE; +} + +/* + * phosh_shell_get_debug_flags: + * + * Get the debug flags + * + * Returns: The global debug flags + */ +PhoshShellDebugFlags +phosh_shell_get_debug_flags (void) +{ + return debug_flags; +} + + +GType +phosh_shell_get_lockscreen_type (PhoshShell *self) +{ + PhoshShellClass *klass = PHOSH_SHELL_GET_CLASS (self); + return klass->get_lockscreen_type (self); +} + + +void +phosh_shell_show_osd (PhoshShell *self, + const char *connector, + const char *icon, + const char *label, + double level, + double max_level) +{ + PhoshShellPrivate *priv = phosh_shell_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_SHELL (self)); + + g_debug ("DBus show osd: connector: %s icon: %s, label: %s, level %f/%f", + connector, icon, label, level, max_level); + + if (priv->osd) { + priv->osd_continue = TRUE; + g_object_set (priv->osd, + "connector", connector, + "label", label, + "icon-name", icon, + "level", level, + "max-level", max_level, + NULL); + } else { + priv->osd = PHOSH_OSD_WINDOW (phosh_osd_window_new (connector, label, icon, level, max_level)); + g_signal_connect_swapped (priv->osd, "destroy", G_CALLBACK (on_osd_destroyed), self); + gtk_widget_set_visible (GTK_WIDGET (priv->osd), TRUE); + } + + if (!priv->osd_timeoutid) { + priv->osd_timeoutid = g_timeout_add_seconds (OSD_HIDE_TIMEOUT, + (GSourceFunc) on_osd_timeout, + self); + g_source_set_name_by_id (priv->osd_timeoutid, "[phosh] osd-timeout"); + } +} + +/* }}} */ diff --git a/src/shell.h b/src/shell.h new file mode 100644 index 000000000..b853ba120 --- /dev/null +++ b/src/shell.h @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include + +#include "lockscreen-manager.h" +#include "screenshot-manager.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SHELL phosh_shell_get_type () + +G_DECLARE_DERIVABLE_TYPE (PhoshShell, phosh_shell, PHOSH, SHELL, GObject) + +struct _PhoshShellClass { + GObjectClass parent_class; + GType (*get_lockscreen_type) (PhoshShell *self); + + /* Padding for future expansion */ + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + +PhoshShell *phosh_shell_new (void); + +void phosh_shell_set_default (PhoshShell *self); +PhoshShell *phosh_shell_get_default (void); + +GType phosh_shell_get_lockscreen_type (PhoshShell *self); +gboolean phosh_shell_get_locked (PhoshShell *self); +void phosh_shell_get_usable_area (PhoshShell *self, + int *x, + int *y, + int *width, + int *height); + +void phosh_shell_fade_out (PhoshShell *self, guint timeout); + +/* Created by the shell on startup */ +PhoshLockscreenManager *phosh_shell_get_lockscreen_manager (PhoshShell *self); +PhoshScreenshotManager *phosh_shell_get_screenshot_manager (PhoshShell *self); + +G_END_DECLS diff --git a/src/splash-manager.c b/src/splash-manager.c new file mode 100644 index 000000000..d14d008f5 --- /dev/null +++ b/src/splash-manager.c @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-splash-manager" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "splash.h" +#include "splash-manager.h" + +#include + +/** + * PhoshSplashManager: + * + * Handles splash screens + * + * Spawns, keeps track and closes splash screens. + */ + +enum { + PROP_0, + PROP_APP_TRACKER, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshSplashManager { + GObject parent; + + PhoshAppTracker *app_tracker; + GHashTable *splashes; + + GSettings *interface_settings; + gboolean prefer_dark; +}; +G_DEFINE_TYPE (PhoshSplashManager, phosh_splash_manager, G_TYPE_OBJECT) + + +static void +phosh_splash_manager_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSplashManager *self = PHOSH_SPLASH_MANAGER (object); + + switch (property_id) { + case PROP_APP_TRACKER: + self->app_tracker = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_splash_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSplashManager *self = PHOSH_SPLASH_MANAGER (object); + + switch (property_id) { + case PROP_APP_TRACKER: + g_value_set_object (value, self->app_tracker); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +on_splash_closed (PhoshSplashManager *self, GtkWidget *splash) +{ + const char *key; + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + g_return_if_fail (PHOSH_IS_SPLASH (splash)); + + key = g_object_get_data (G_OBJECT (splash), "startup-id"); + + g_return_if_fail (g_hash_table_remove (self->splashes, key)); +} + + +static void +on_app_ready (PhoshSplashManager *self, + GDesktopAppInfo *info, + const char *startup_id, + PhoshAppTracker *tracker) +{ + PhoshSplash *splash; + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + g_return_if_fail (startup_id); + g_debug ("Removing splash for %s, startup_id %s", g_app_info_get_id (G_APP_INFO (info)), + startup_id); + + splash = g_hash_table_lookup (self->splashes, startup_id); + /* E.g. firefox sends the same startup id for multiple windows */ + if (!splash) { + g_debug ("No splash for startup_id %s", startup_id); + return; + } + + g_hash_table_remove (self->splashes, startup_id); +} + + +static void +on_app_failed (PhoshSplashManager *self, + GDesktopAppInfo *info, + const char *startup_id, + PhoshAppTracker *tracker) +{ + GtkWidget *splash; + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + g_return_if_fail (startup_id); + g_debug ("Removing splash for failed %s, startup_id %s", + g_app_info_get_id (G_APP_INFO (info)), + startup_id); + + splash = g_hash_table_lookup (self->splashes, startup_id); + /* E.g. firefox sends the same startup id for multiple windows */ + if (!splash) { + g_debug ("No splash for startup_id %s", startup_id); + return; + } + + /* TODO: show failed splash once we have designs */ + g_hash_table_remove (self->splashes, startup_id); +} + + +static void +on_app_launch_started (PhoshSplashManager *self, + GDesktopAppInfo *info, + const char *startup_id, + PhoshAppTracker *tracker) +{ + GtkWidget *splash; + char *key; + PhoshShell *shell = phosh_shell_get_default (); + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + g_return_if_fail (G_IS_DESKTOP_APP_INFO (info)); + g_return_if_fail (startup_id); + g_return_if_fail (!g_hash_table_contains (self->splashes, startup_id)); + + if (!phosh_shell_get_show_splash (shell)) + return; + + g_debug ("Adding splash for %s, startup_id %s", g_app_info_get_id (G_APP_INFO (info)), + startup_id); + splash = phosh_splash_new (info, self->prefer_dark); + key = g_strdup (startup_id); + g_hash_table_insert (self->splashes, key, splash); + g_signal_connect_object (splash, "closed", G_CALLBACK (on_splash_closed), + self, G_CONNECT_SWAPPED); + /* Keep startup-id for close triggered by splash itself */ + g_object_set_data (G_OBJECT (splash), "startup-id", key); + gtk_window_present (GTK_WINDOW (splash)); +} + + +static void +gsettings_color_scheme_changed_cb (PhoshSplashManager *self) +{ + self->prefer_dark = (g_settings_get_enum (self->interface_settings, "color-scheme") == + G_DESKTOP_COLOR_SCHEME_PREFER_DARK); +} + + +static void +phosh_splash_manager_constructed (GObject *object) +{ + PhoshSplashManager *self = PHOSH_SPLASH_MANAGER (object); + + G_OBJECT_CLASS (phosh_splash_manager_parent_class)->constructed (object); + + g_object_connect (self->app_tracker, + "swapped-signal::app-launch-started", G_CALLBACK (on_app_launch_started), self, + "swapped-signal::app-failed", G_CALLBACK (on_app_failed), self, + "swapped-signal::app-ready", G_CALLBACK (on_app_ready), self, + NULL); +} + + +static void +phosh_splash_manager_dispose (GObject *object) +{ + PhoshSplashManager *self = PHOSH_SPLASH_MANAGER (object); + + g_clear_object (&self->interface_settings); + g_clear_object (&self->app_tracker); + + G_OBJECT_CLASS (phosh_splash_manager_parent_class)->dispose (object); +} + + +static void +phosh_splash_manager_finalize (GObject *object) +{ + PhoshSplashManager *self = PHOSH_SPLASH_MANAGER (object); + + g_clear_pointer (&self->splashes, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_splash_manager_parent_class)->finalize (object); +} + + +static void +phosh_splash_manager_class_init (PhoshSplashManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_splash_manager_get_property; + object_class->set_property = phosh_splash_manager_set_property; + object_class->constructed = phosh_splash_manager_constructed; + object_class->dispose = phosh_splash_manager_dispose; + object_class->finalize = phosh_splash_manager_finalize; + + props[PROP_APP_TRACKER] = + g_param_spec_object ("app-tracker", "", "", + PHOSH_TYPE_APP_TRACKER, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_splash_manager_init (PhoshSplashManager *self) +{ + self->splashes = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + (GDestroyNotify) phosh_splash_hide); + + self->interface_settings = g_settings_new ("org.gnome.desktop.interface"); + g_signal_connect_swapped (self->interface_settings, + "changed::color-scheme", + G_CALLBACK (gsettings_color_scheme_changed_cb), + self); + gsettings_color_scheme_changed_cb (self); +} + + +PhoshSplashManager * +phosh_splash_manager_new (PhoshAppTracker *app_tracker) +{ + return PHOSH_SPLASH_MANAGER (g_object_new (PHOSH_TYPE_SPLASH_MANAGER, + "app-tracker", app_tracker, + NULL)); +} + + +void +phosh_splash_manager_lower_all (PhoshSplashManager *self) +{ + GHashTableIter iter; + gpointer key, value; + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + + g_hash_table_iter_init (&iter, self->splashes); + + while (g_hash_table_iter_next (&iter, &key, &value)) { + PhoshSplash *splash = PHOSH_SPLASH (value); + + phosh_splash_lower (splash); + } +} + + +void +phosh_splash_manager_raise (PhoshSplashManager *self, const char *startup_id) +{ + PhoshSplash *splash; + + g_return_if_fail (PHOSH_IS_SPLASH_MANAGER (self)); + + splash = g_hash_table_lookup (self->splashes, startup_id); + if (splash == NULL) { + g_warning ("Invalid startup-id %s", startup_id); + return; + } + + phosh_splash_raise (splash); +} + + +gboolean +phosh_splash_manager_get_prefer_dark (PhoshSplashManager *self) +{ + g_return_val_if_fail (PHOSH_IS_SPLASH_MANAGER (self), FALSE); + + return self->prefer_dark; +} diff --git a/src/splash-manager.h b/src/splash-manager.h new file mode 100644 index 000000000..424241af5 --- /dev/null +++ b/src/splash-manager.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "app-tracker.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SPLASH_MANAGER (phosh_splash_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshSplashManager, phosh_splash_manager, PHOSH, SPLASH_MANAGER, GObject) + +PhoshSplashManager *phosh_splash_manager_new (PhoshAppTracker *app_tracker); +void phosh_splash_manager_lower_all (PhoshSplashManager *self); +void phosh_splash_manager_raise (PhoshSplashManager *self, const char *startup_id); +gboolean phosh_splash_manager_get_prefer_dark (PhoshSplashManager *self); + +G_END_DECLS diff --git a/src/splash.c b/src/splash.c new file mode 100644 index 000000000..1df7c6f83 --- /dev/null +++ b/src/splash.c @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-splash" + +#include "phosh-config.h" + +#include "animation.h" +#include "layersurface-priv.h" +#include "phosh-wayland.h" +#include "monitor.h" +#include "shell-priv.h" +#include "splash.h" + +#define PHOSH_APP_UNKNOWN_ICON "app-icon-unknown" + +/** + * PhoshSplash: + * + * A splash screen + * + * The #PhoshSplash is a splash screen used to indicate application + * startup. + */ + +enum { + PROP_0, + PROP_APP_INFO, + PROP_PREFER_DARK, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +typedef struct { + struct zwlr_layer_shell_v1 *layer_shell; + + GAppInfo *info; + GtkWidget *box; + GIcon *icon; + GtkWidget *img_app; + gboolean prefer_dark; + + PhoshAnimation *fadeout; +} PhoshSplashPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSplash, phosh_splash, PHOSH_TYPE_LAYER_SURFACE); + + +static void +set_style (PhoshSplash *self, gboolean prefer_dark) +{ + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + GtkStyleContext *context; + + priv->prefer_dark = prefer_dark; + + context = gtk_widget_get_style_context (GTK_WIDGET (self)); + if (prefer_dark) { + gtk_style_context_add_class (context, "dark"); + gtk_style_context_remove_class (context, "light"); + } else { + gtk_style_context_add_class (context, "light"); + gtk_style_context_remove_class (context, "dark"); + } +} + + +static void +phosh_splash_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSplash *self = PHOSH_SPLASH (obj); + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + + switch (prop_id) { + case PROP_APP_INFO: + g_set_object (&priv->info, g_value_get_object (value)); + break; + case PROP_PREFER_DARK: + set_style (self, g_value_get_boolean (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_splash_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSplash *self = PHOSH_SPLASH (obj); + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + + switch (prop_id) { + case PROP_APP_INFO: + g_value_set_object (value, priv->info); + break; + case PROP_PREFER_DARK: + g_value_set_boolean (value, priv->prefer_dark); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_splash_dispose (GObject *obj) +{ + PhoshSplash *self = PHOSH_SPLASH (obj); + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + + g_clear_pointer (&priv->fadeout, phosh_animation_unref); + g_clear_object (&priv->info); + + G_OBJECT_CLASS (phosh_splash_parent_class)->dispose (obj); +} + + +static void +phosh_splash_constructed (GObject *object) +{ + PhoshSplash *self = PHOSH_SPLASH (object); + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + PhoshMonitor *monitor; + + g_debug ("New splash for %s", g_app_info_get_id (priv->info)); + monitor = phosh_shell_get_primary_monitor (phosh_shell_get_default ()); + + g_object_set (PHOSH_LAYER_SURFACE (self), + "layer-shell", phosh_wayland_get_zwlr_layer_shell_v1 (wl), + "wl-output", phosh_monitor_get_wl_output (monitor), + "anchor", (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT), + "layer", ZWLR_LAYER_SHELL_V1_LAYER_TOP, + "kbd-interactivity", TRUE, + "exclusive-zone", -1, + "namespace", "phosh splash", + NULL); + + G_OBJECT_CLASS (phosh_splash_parent_class)->constructed (object); +} + + +static gboolean +phosh_splash_key_press_event (GtkWidget *self, GdkEventKey *event) +{ + gboolean handled = FALSE; + + g_return_val_if_fail (PHOSH_IS_SPLASH (self), FALSE); + + switch (event->keyval) { + case GDK_KEY_Escape: + g_signal_emit (self, signals[CLOSED], 0); + handled = TRUE; + break; + default: + /* nothing to do */ + break; + } + + return handled; +} + + +static void +phosh_splash_show (GtkWidget *widget) +{ + PhoshSplash *self = PHOSH_SPLASH (widget); + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + GIcon *icon; + + icon = g_app_info_get_icon (priv->info); + if (G_UNLIKELY (icon == NULL)) { + gtk_image_set_from_icon_name (GTK_IMAGE (priv->img_app), + PHOSH_APP_UNKNOWN_ICON, + -1); + } else { + gtk_image_set_from_gicon (GTK_IMAGE (priv->img_app), icon, -1); + } + + GTK_WIDGET_CLASS (phosh_splash_parent_class)->show (widget); +} + + +static void +phosh_splash_class_init (PhoshSplashClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_splash_get_property; + object_class->set_property = phosh_splash_set_property; + object_class->constructed = phosh_splash_constructed; + object_class->dispose = phosh_splash_dispose; + widget_class->show = phosh_splash_show; + widget_class->key_press_event = phosh_splash_key_press_event; + + /** + * PhoshSplash:app-info: + * + * The appinfo this splash is for + */ + props[PROP_APP_INFO] = g_param_spec_object ("app-info", "", "", + G_TYPE_DESKTOP_APP_INFO, + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + /** + * PhoshSplash:prefer-dark: + * + * Whether the splash should prefer dark theming + */ + props[PROP_PREFER_DARK] = g_param_spec_boolean ("prefer-dark", "", "", + FALSE, + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshSplay:closed: + * + * The splash should be closed + */ + signals[CLOSED] = g_signal_new ("closed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, + G_TYPE_NONE, + 0); + + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/splash.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSplash, img_app); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSplash, box); + + gtk_widget_class_set_css_name (widget_class, "phosh-splash"); +} + + +static void +phosh_splash_init (PhoshSplash *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_splash_new (GDesktopAppInfo *info, gboolean prefer_dark) +{ + return g_object_new (PHOSH_TYPE_SPLASH, + "app-info", info, + "prefer-dark", prefer_dark, + NULL); +} + + +static void +fadeout_value_cb (double value, gpointer data) +{ + PhoshSplash *self = data; + + phosh_layer_surface_set_alpha (PHOSH_LAYER_SURFACE (self), 1.0 - value); +} + + +static void +fadeout_done_cb (gpointer data) +{ + PhoshSplash *self = data; + PhoshSplashPrivate *priv = phosh_splash_get_instance_private (self); + + g_clear_pointer (&priv->fadeout, phosh_animation_unref); + + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +void +phosh_splash_hide (PhoshSplash *self) +{ + PhoshSplashPrivate *priv; + + g_return_if_fail (PHOSH_IS_SPLASH (self)); + priv = phosh_splash_get_instance_private (self); + + priv->fadeout = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + 200 * PHOSH_ANIMATION_SLOWDOWN, + PHOSH_ANIMATION_TYPE_EASE_IN_QUINTIC, + fadeout_value_cb, + fadeout_done_cb, + self); + phosh_animation_start (priv->fadeout); +} + + +void +phosh_splash_lower (PhoshSplash *self) +{ + g_return_if_fail (PHOSH_IS_SPLASH (self)); + + phosh_layer_surface_set_layer (PHOSH_LAYER_SURFACE (self), + ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM); + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +void +phosh_splash_raise (PhoshSplash *self) +{ + g_return_if_fail (PHOSH_IS_SPLASH (self)); + + phosh_layer_surface_set_layer (PHOSH_LAYER_SURFACE (self), + ZWLR_LAYER_SHELL_V1_LAYER_TOP); + gtk_widget_queue_draw (GTK_WIDGET (self)); +} diff --git a/src/splash.h b/src/splash.h new file mode 100644 index 000000000..7c2509db5 --- /dev/null +++ b/src/splash.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "layersurface.h" + +#include + +#define PHOSH_TYPE_SPLASH (phosh_splash_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshSplash, phosh_splash, PHOSH, SPLASH, PhoshLayerSurface) + +/** + * PhoshSplashClass + * @parent_class: The parent class + */ +struct _PhoshSplashClass { + PhoshLayerSurfaceClass parent_class; +}; + + +GtkWidget *phosh_splash_new (GDesktopAppInfo *info, gboolean prefer_dark); +void phosh_splash_hide (PhoshSplash *self); +void phosh_splash_lower (PhoshSplash *self); +void phosh_splash_raise (PhoshSplash *self); diff --git a/src/status-icon.c b/src/status-icon.c new file mode 100644 index 000000000..eb6802d8b --- /dev/null +++ b/src/status-icon.c @@ -0,0 +1,490 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Julian Sparber + */ + +#define G_LOG_DOMAIN "phosh-status-icon" + +#include "phosh-config.h" + +#include "status-icon.h" + +/** + * PhoshStatusIcon: + * + * Base class for status icons used in the Phosh's top-bar or in + * [type@QuickSetting]s. It's very common to have the same status icon + * class used for both places. + * + * If the widget will be used in a [type@QuickSetting] it is + * recommended (but not required) that derived classes implement a + * `enabled` property. + */ + +enum { + PHOSH_STATUS_ICON_PROP_0, + PHOSH_STATUS_ICON_PROP_ICON_NAME, + PHOSH_STATUS_ICON_PROP_ICON_SIZE, + PHOSH_STATUS_ICON_PROP_PIXEL_SIZE, + PHOSH_STATUS_ICON_PROP_EXTRA_WIDGET, + PHOSH_STATUS_ICON_PROP_INFO, + PHOSH_STATUS_ICON_PROP_LAST_PROP +}; +static GParamSpec *props[PHOSH_STATUS_ICON_PROP_LAST_PROP]; + +typedef struct { + GtkBox *box; + GtkWidget *image; + GtkWidget *extra_widget; + GtkIconSize icon_size; + guint pixel_size; + char *info; + + guint idle_id; +} PhoshStatusIconPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshStatusIcon, phosh_status_icon, GTK_TYPE_BIN); + + +static void +phosh_status_icon_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshStatusIcon *self = PHOSH_STATUS_ICON (object); + + switch (property_id) { + case PHOSH_STATUS_ICON_PROP_ICON_NAME: + phosh_status_icon_set_icon_name (self, g_value_get_string (value)); + break; + case PHOSH_STATUS_ICON_PROP_ICON_SIZE: + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + phosh_status_icon_set_icon_size (self, g_value_get_enum (value)); + G_GNUC_END_IGNORE_DEPRECATIONS + break; + case PHOSH_STATUS_ICON_PROP_PIXEL_SIZE: + phosh_status_icon_set_pixel_size (self, g_value_get_uint (value)); + break; + case PHOSH_STATUS_ICON_PROP_EXTRA_WIDGET: + phosh_status_icon_set_extra_widget (self, g_value_get_object (value)); + break; + case PHOSH_STATUS_ICON_PROP_INFO: + phosh_status_icon_set_info (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_status_icon_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshStatusIcon *self = PHOSH_STATUS_ICON (object); + + switch (property_id) { + case PHOSH_STATUS_ICON_PROP_ICON_NAME: + g_value_take_string (value, phosh_status_icon_get_icon_name (self)); + break; + case PHOSH_STATUS_ICON_PROP_ICON_SIZE: + G_GNUC_BEGIN_IGNORE_DEPRECATIONS + g_value_set_enum (value, phosh_status_icon_get_icon_size (self)); + G_GNUC_END_IGNORE_DEPRECATIONS + break; + case PHOSH_STATUS_ICON_PROP_PIXEL_SIZE: + g_value_set_uint (value, phosh_status_icon_get_pixel_size (self)); + break; + case PHOSH_STATUS_ICON_PROP_EXTRA_WIDGET: + g_value_set_object (value, phosh_status_icon_get_extra_widget (self)); + break; + case PHOSH_STATUS_ICON_PROP_INFO: + g_value_set_string (value, phosh_status_icon_get_info (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static gboolean +on_idle (PhoshStatusIcon *self) +{ + PhoshStatusIconClass *klass = PHOSH_STATUS_ICON_GET_CLASS (self); + PhoshStatusIconPrivate *priv = phosh_status_icon_get_instance_private (self); + + if (klass->idle_init) + (*klass->idle_init) (self); + + priv->idle_id = 0; + return G_SOURCE_REMOVE; +} + + +static void +phosh_status_icon_constructed (GObject *object) +{ + PhoshStatusIcon *self = PHOSH_STATUS_ICON (object); + PhoshStatusIconPrivate *priv = phosh_status_icon_get_instance_private (self); + PhoshStatusIconClass *klass = PHOSH_STATUS_ICON_GET_CLASS (self); + + G_OBJECT_CLASS (phosh_status_icon_parent_class)->constructed (object); + + if (klass->idle_init) { + priv->idle_id = g_idle_add ((GSourceFunc) on_idle, self); + g_source_set_name_by_id (priv->idle_id, "[PhoshStatusIcon] idle"); + } +} + + +static void +phosh_status_icon_dispose (GObject *object) +{ + PhoshStatusIcon *self = PHOSH_STATUS_ICON (object); + PhoshStatusIconPrivate *priv = phosh_status_icon_get_instance_private (self); + + g_clear_handle_id (&priv->idle_id, g_source_remove); + + G_OBJECT_CLASS (phosh_status_icon_parent_class)->dispose (object); +} + + +static void +phosh_status_icon_finalize (GObject *gobject) +{ + PhoshStatusIconPrivate *priv = + phosh_status_icon_get_instance_private (PHOSH_STATUS_ICON (gobject)); + + g_clear_pointer (&priv->info, g_free); + + G_OBJECT_CLASS (phosh_status_icon_parent_class)->finalize (gobject); +} + + +static void +phosh_status_icon_destroy (GtkWidget *widget) +{ + PhoshStatusIcon *self = PHOSH_STATUS_ICON (widget); + + phosh_status_icon_set_extra_widget (self, NULL); + + GTK_WIDGET_CLASS (phosh_status_icon_parent_class)->destroy (widget); +} + +static void +phosh_status_icon_class_init (PhoshStatusIconClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_status_icon_set_property; + object_class->get_property = phosh_status_icon_get_property; + object_class->constructed = phosh_status_icon_constructed; + object_class->dispose = phosh_status_icon_dispose; + object_class->finalize = phosh_status_icon_finalize; + + widget_class->destroy = phosh_status_icon_destroy; + + gtk_widget_class_set_css_name (widget_class, "phosh-status-icon"); + + /** + * PhoshStatusIcon:icon-name: + * + * The name of the icon to display in the widget + */ + props[PHOSH_STATUS_ICON_PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshStatusIcon:icon-size: + * + * The size of the icon to display in the widget + */ + props[PHOSH_STATUS_ICON_PROP_ICON_SIZE] = + g_param_spec_enum ("icon-size", "", "", + GTK_TYPE_ICON_SIZE, + GTK_ICON_SIZE_LARGE_TOOLBAR, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshStatusIcon:pixel-size: + * + * The size of the icon to display in the widget + */ + props[PHOSH_STATUS_ICON_PROP_PIXEL_SIZE] = + g_param_spec_uint ("pixel-size", "", "", + 0, G_MAXUINT, 24, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshStatusIcon:extra-widget: + * + * An extra widget to display. This is used for extra information when + * used in PhoshTopPanel. When used in [type@QuickSetting] this + * is not needed. + */ + props[PHOSH_STATUS_ICON_PROP_EXTRA_WIDGET] = + g_param_spec_object ("extra_widget", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshStatusIcon:info: + * + * Textual information to display. Think of it as the [type@StatusIcon]'s + * label. + */ + props[PHOSH_STATUS_ICON_PROP_INFO] = + g_param_spec_string ("info", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PHOSH_STATUS_ICON_PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/status-icon.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusIcon, box); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusIcon, image); +} + + +static void +phosh_status_icon_init (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv = phosh_status_icon_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->icon_size = GTK_ICON_SIZE_LARGE_TOOLBAR; + priv->pixel_size = 24; +} + + +GtkWidget * +phosh_status_icon_new (void) +{ + return g_object_new (PHOSH_TYPE_STATUS_ICON, NULL); +} + + +void +phosh_status_icon_set_icon_name (PhoshStatusIcon *self, const char *icon_name) +{ + PhoshStatusIconPrivate *priv; + g_autofree char *old_icon_name = NULL; + + g_return_if_fail (PHOSH_IS_STATUS_ICON (self)); + + priv = phosh_status_icon_get_instance_private (self); + + old_icon_name = phosh_status_icon_get_icon_name (self); + if (!g_strcmp0 (old_icon_name, icon_name)) + return; + + gtk_image_set_from_icon_name (GTK_IMAGE (priv->image), icon_name, -1); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_STATUS_ICON_PROP_ICON_NAME]); +} + + +char * +phosh_status_icon_get_icon_name (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv; + char *icon_name; + + g_return_val_if_fail (PHOSH_IS_STATUS_ICON (self), 0); + + priv = phosh_status_icon_get_instance_private (self); + + g_object_get (priv->image, "icon-name", &icon_name, NULL); + + return icon_name; +} + +/** + * phosh_status_icon_set_icon_size: + * @self: The status-icon + * @size: The size of icon + * + * Set the size of status-icon. + * + * Deprecated: 0.47: Use [method@Phosh.StatusIcon.set_pixel_size]. + */ +void +phosh_status_icon_set_icon_size (PhoshStatusIcon *self, GtkIconSize size) +{ + PhoshStatusIconPrivate *priv; + guint pixel_size; + g_return_if_fail (PHOSH_IS_STATUS_ICON (self)); + + priv = phosh_status_icon_get_instance_private (self); + + if (priv->icon_size == size) + return; + + priv->icon_size = size; + + switch (size) { + case GTK_ICON_SIZE_INVALID: + pixel_size = 0; + break; + case GTK_ICON_SIZE_MENU: + pixel_size = 16; + break; + case GTK_ICON_SIZE_SMALL_TOOLBAR: + pixel_size = 16; + break; + case GTK_ICON_SIZE_LARGE_TOOLBAR: + pixel_size = 24; + break; + case GTK_ICON_SIZE_BUTTON: + pixel_size = 16; + break; + case GTK_ICON_SIZE_DND: + pixel_size = 32; + break; + case GTK_ICON_SIZE_DIALOG: + pixel_size = 48; + break; + default: + g_critical ("Unknown size %d", size); + return; + } + + phosh_status_icon_set_pixel_size (self, pixel_size); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_STATUS_ICON_PROP_ICON_SIZE]); +} + +/** + * phosh_status_icon_get_icon_size: + * @self: The status-icon + * + * Return the size of status-icon. + * + * Returns: The size of status-icon. + * + * Deprecated: 0.47: Use [method@Phosh.StatusIcon.get_pixel_size]. + */ +GtkIconSize +phosh_status_icon_get_icon_size (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_ICON (self), 0); + + priv = phosh_status_icon_get_instance_private (self); + + return priv->icon_size; +} + + +void +phosh_status_icon_set_pixel_size (PhoshStatusIcon *self, guint size) +{ + PhoshStatusIconPrivate *priv; + g_return_if_fail (PHOSH_IS_STATUS_ICON (self)); + + priv = phosh_status_icon_get_instance_private (self); + + if (priv->pixel_size == size) + return; + + priv->pixel_size = size; + gtk_image_set_pixel_size (GTK_IMAGE (priv->image), priv->pixel_size); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_STATUS_ICON_PROP_PIXEL_SIZE]); +} + + +guint +phosh_status_icon_get_pixel_size (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_ICON (self), 0); + + priv = phosh_status_icon_get_instance_private (self); + + return priv->pixel_size; +} + + +void +phosh_status_icon_set_extra_widget (PhoshStatusIcon *self, GtkWidget *widget) +{ + PhoshStatusIconPrivate *priv; + g_return_if_fail (PHOSH_IS_STATUS_ICON (self)); + + priv = phosh_status_icon_get_instance_private (self); + + if (priv->extra_widget == widget) + return; + + if (priv->extra_widget != NULL) + gtk_container_remove (GTK_CONTAINER (priv->box), priv->extra_widget); + + if (widget != NULL) + gtk_container_add (GTK_CONTAINER (priv->box), widget); + + priv->extra_widget = widget; + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_STATUS_ICON_PROP_EXTRA_WIDGET]); +} + +/** + * phosh_status_icon_get_extra_widget: + * @self: A status icon + * + * Get the extra widget or %NULL if there's no extra widget + * + * Returns:(transfer none)(nullable): The extra widget + */ +GtkWidget * +phosh_status_icon_get_extra_widget (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_ICON (self), 0); + + priv = phosh_status_icon_get_instance_private (self); + + return priv->extra_widget; +} + + +char * +phosh_status_icon_get_info (PhoshStatusIcon *self) +{ + PhoshStatusIconPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_ICON (self), 0); + + priv = phosh_status_icon_get_instance_private (self); + + return priv->info; +} + + +void +phosh_status_icon_set_info (PhoshStatusIcon *self, const char *info) +{ + PhoshStatusIconPrivate *priv; + g_return_if_fail (PHOSH_IS_STATUS_ICON (self)); + + priv = phosh_status_icon_get_instance_private (self); + + if (g_strcmp0 (priv->info, info) == 0) + return; + + g_clear_pointer (&priv->info, g_free); + priv->info = g_strdup (info); + + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_STATUS_ICON_PROP_INFO]); +} diff --git a/src/status-icon.h b/src/status-icon.h new file mode 100644 index 000000000..9e7527745 --- /dev/null +++ b/src/status-icon.h @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_STATUS_ICON (phosh_status_icon_get_type()) + +G_DECLARE_DERIVABLE_TYPE (PhoshStatusIcon, phosh_status_icon, PHOSH, STATUS_ICON, GtkBin) + +/** + * PhoshStatusIconClass: + * @parent_class: The parent class + * @idle_init: a callback to be invoked once on idle + */ +struct _PhoshStatusIconClass { + GtkBinClass parent_class; + + void (*idle_init) (PhoshStatusIcon *self); + + /* Padding for future expansion */ + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + +GtkWidget * phosh_status_icon_new (void); +void phosh_status_icon_set_icon_size (PhoshStatusIcon *self, GtkIconSize size) G_GNUC_DEPRECATED_FOR (phosh_status_icon_set_pixel_size); +GtkIconSize phosh_status_icon_get_icon_size (PhoshStatusIcon *self) G_GNUC_DEPRECATED_FOR (phosh_status_icon_get_pixel_size); +void phosh_status_icon_set_pixel_size (PhoshStatusIcon *self, guint size); +guint phosh_status_icon_get_pixel_size (PhoshStatusIcon *self); +void phosh_status_icon_set_icon_name (PhoshStatusIcon *self, const char *icon_name); +char *phosh_status_icon_get_icon_name (PhoshStatusIcon *self); +void phosh_status_icon_set_extra_widget (PhoshStatusIcon *self, GtkWidget *widget); +GtkWidget * phosh_status_icon_get_extra_widget (PhoshStatusIcon *self); +void phosh_status_icon_set_info (PhoshStatusIcon *self, const char *info); +char *phosh_status_icon_get_info (PhoshStatusIcon *self); + +G_END_DECLS diff --git a/src/status-page-placeholder.c b/src/status-page-placeholder.c new file mode 100644 index 000000000..ee5511681 --- /dev/null +++ b/src/status-page-placeholder.c @@ -0,0 +1,299 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-status-page-placeholder" + +#include "phosh-config.h" + +#include "status-page-placeholder.h" + +#include + +/** + * PhoshStatusPagePlaceholder: + * + * A placeholder in a [class@StatusPage]. + * + * The placeholder page has a title and an icon and can have a single + * child which is put below the title. + * + * This widget can be replaced with `AdwStatusPage` and a bit of styling + * once we switch to GTK4. + */ + +enum { + PROP_0, + PROP_TITLE, + PROP_ICON_NAME, + PROP_EXTRA_WIDGET, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshStatusPagePlaceholder { + GtkBin parent; + + GtkBox *toplevel_box; + + GtkImage *icon; + char *icon_name; + GtkLabel *title_label; + + GtkWidget *extra_widget; +}; +G_DEFINE_TYPE (PhoshStatusPagePlaceholder, phosh_status_page_placeholder, GTK_TYPE_BIN) + + +static void +update_title_visibility (PhoshStatusPagePlaceholder *self) +{ + const char *text = gtk_label_get_text (self->title_label); + + gtk_widget_set_visible (GTK_WIDGET (self->title_label), !gm_str_is_null_or_empty (text)); +} + + +static void +phosh_status_page_placeholder_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + switch (property_id) { + case PROP_TITLE: + phosh_status_page_placeholder_set_title (self, g_value_get_string (value)); + break; + case PROP_ICON_NAME: + phosh_status_page_placeholder_set_icon_name (self, g_value_get_string (value)); + break; + case PROP_EXTRA_WIDGET: + phosh_status_page_placeholder_set_extra_widget (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_status_page_placeholder_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + switch (property_id) { + case PROP_TITLE: + g_value_set_string (value, phosh_status_page_placeholder_get_title (self)); + break; + case PROP_ICON_NAME: + g_value_set_string (value, phosh_status_page_placeholder_get_icon_name (self)); + break; + case PROP_EXTRA_WIDGET: + g_value_set_object (value, phosh_status_page_placeholder_get_extra_widget (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_status_page_placeholder_dispose (GObject *object) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (object); + + g_clear_pointer (&self->icon_name, g_free); + + G_OBJECT_CLASS (phosh_status_page_placeholder_parent_class)->dispose (object); +} + + +static void +phosh_status_page_placeholder_destroy (GtkWidget *widget) +{ + PhoshStatusPagePlaceholder *self = PHOSH_STATUS_PAGE_PLACEHOLDER (widget); + + phosh_status_page_placeholder_set_extra_widget (self, NULL); + + GTK_WIDGET_CLASS (phosh_status_page_placeholder_parent_class)->destroy (widget); +} + + +static void +phosh_status_page_placeholder_class_init (PhoshStatusPagePlaceholderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_status_page_placeholder_get_property; + object_class->set_property = phosh_status_page_placeholder_set_property; + object_class->dispose = phosh_status_page_placeholder_dispose; + + widget_class->destroy = phosh_status_page_placeholder_destroy; + + /** + * PhoshStatusPagePlaceholder:title: + * + * The title of the placeholder page, displayed below the icon. + */ + props[PROP_TITLE] = + g_param_spec_string ("title", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPagePlaceholder:icon-name: + * + * The name of the icon on the placeholder page + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPagePlaceholder:extra-widget: + * + * An extra widget to add to bottom of the placeholder page. + */ + props[PROP_EXTRA_WIDGET] = + g_param_spec_object ("extra-widget", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/status-page-placeholder.ui"); + + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, icon); + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, title_label); + gtk_widget_class_bind_template_child (widget_class, PhoshStatusPagePlaceholder, toplevel_box); + + gtk_widget_class_set_css_name (widget_class, "phosh-status-page-placeholder"); +} + + +static void +phosh_status_page_placeholder_init (PhoshStatusPagePlaceholder *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); + + update_title_visibility (self); +} + + +PhoshStatusPagePlaceholder * +phosh_status_page_placeholder_new (void) +{ + return g_object_new (PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER, NULL); +} + + +void +phosh_status_page_placeholder_set_title (PhoshStatusPagePlaceholder *self, const char *title) +{ + const char *current; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self)); + + current = gtk_label_get_label (self->title_label); + if (g_strcmp0 (current, title) == 0) + return; + + gtk_label_set_label (self->title_label, title); + update_title_visibility (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + + +const char * +phosh_status_page_placeholder_get_title (PhoshStatusPagePlaceholder *self) +{ + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self), NULL); + + return gtk_label_get_label (self->title_label); +} + + +void +phosh_status_page_placeholder_set_icon_name (PhoshStatusPagePlaceholder *self, const char *icon_name) +{ + g_return_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self)); + + if (g_strcmp0 (self->icon_name, icon_name) == 0) + return; + + g_free (self->icon_name); + self->icon_name = g_strdup (icon_name); + + if (!icon_name) + g_object_set (G_OBJECT (self->icon), "icon-name", "image-missing", NULL); + else + g_object_set (G_OBJECT (self->icon), "icon-name", icon_name, NULL); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); +} + + +const char * +phosh_status_page_placeholder_get_icon_name (PhoshStatusPagePlaceholder *self) +{ + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self), NULL); + + return self->icon_name; +} + +/** + * phosh_status_page_placeholder_set_extra_widget: + * @self: A status page placeholder + * + * Set the extra widget shown at the bottom of a status page placeholder. Use `NULL` to remove + * existing widget. + */ +void +phosh_status_page_placeholder_set_extra_widget (PhoshStatusPagePlaceholder *self, GtkWidget *extra_widget) +{ + g_return_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self)); + g_return_if_fail (extra_widget == NULL || GTK_IS_WIDGET (extra_widget)); + + if (self->extra_widget == extra_widget) + return; + + if (self->extra_widget) + gtk_container_remove (GTK_CONTAINER (self->toplevel_box), self->extra_widget); + + self->extra_widget = extra_widget; + + if (self->extra_widget) + gtk_container_add (GTK_CONTAINER (self->toplevel_box), self->extra_widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXTRA_WIDGET]); +} + +/** + * phosh_status_page_placeholder_get_extra_widget: + * @self: A status page placeholder + * + * Get the extra_widget of the status page placeholder. + * + * Returns:(transfer none): The status page placeholder extra_widget + */ +GtkWidget * +phosh_status_page_placeholder_get_extra_widget (PhoshStatusPagePlaceholder *self) +{ + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE_PLACEHOLDER (self), NULL); + + return self->extra_widget; +} diff --git a/src/status-page-placeholder.h b/src/status-page-placeholder.h new file mode 100644 index 000000000..58a3f95f1 --- /dev/null +++ b/src/status-page-placeholder.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER (phosh_status_page_placeholder_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshStatusPagePlaceholder, phosh_status_page_placeholder, PHOSH, STATUS_PAGE_PLACEHOLDER, GtkBin) + +PhoshStatusPagePlaceholder *phosh_status_page_placeholder_new (void); +void phosh_status_page_placeholder_set_title (PhoshStatusPagePlaceholder *self, + const char *title); +const char *phosh_status_page_placeholder_get_title (PhoshStatusPagePlaceholder *self); +void phosh_status_page_placeholder_set_icon_name (PhoshStatusPagePlaceholder *self, + const char *icon_name); +const char *phosh_status_page_placeholder_get_icon_name (PhoshStatusPagePlaceholder *self); +void phosh_status_page_placeholder_set_extra_widget (PhoshStatusPagePlaceholder *self, GtkWidget *extra_widget); +GtkWidget *phosh_status_page_placeholder_get_extra_widget (PhoshStatusPagePlaceholder *self); + +G_END_DECLS diff --git a/src/status-page.c b/src/status-page.c new file mode 100644 index 000000000..52b1c8f67 --- /dev/null +++ b/src/status-page.c @@ -0,0 +1,403 @@ +/* + * Copyright (C) 2023 Tether Operations Limited + * 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authors: Arun Mani J + * Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-status-page" + +#include "status-page.h" +#include "status-page-placeholder.h" + +/** + * PhoshStatusPage: + * + * Additional status information associated with a [class@QuickSetting]. + * + * This is displayed when the quick setting needs to show status. + */ + +enum { + PROP_0, + PROP_TITLE, + PROP_HEADER, + PROP_CONTENT, + PROP_FOOTER, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + DONE, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +typedef struct { + GtkBox *toplevel_box; + + /* Header */ + GtkLabel *title_label; + GtkBox *header_bin; + GtkWidget *header_widget; + + /* Content */ + GtkBox *content_bin; + GtkWidget *content_widget; + + /* Footer */ + GtkSeparator *footer_separator; + GtkBox *footer_bin; + GtkWidget *footer_widget; +} PhoshStatusPagePrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshStatusPage, phosh_status_page, GTK_TYPE_BIN); + + +static void +phosh_status_page_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (object); + + switch (property_id) { + case PROP_TITLE: + phosh_status_page_set_title (self, g_value_get_string (value)); + break; + case PROP_HEADER: + phosh_status_page_set_header (self, g_value_get_object (value)); + break; + case PROP_CONTENT: + phosh_status_page_set_content (self, g_value_get_object (value)); + break; + case PROP_FOOTER: + phosh_status_page_set_footer (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_status_page_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (object); + + switch (property_id) { + case PROP_TITLE: + g_value_set_string (value, phosh_status_page_get_title (self)); + break; + case PROP_HEADER: + g_value_set_object (value, phosh_status_page_get_header (self)); + break; + case PROP_CONTENT: + g_value_set_object (value, phosh_status_page_get_content (self)); + break; + case PROP_FOOTER: + g_value_set_object (value, phosh_status_page_get_footer (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + + +static void +phosh_status_page_destroy (GtkWidget *widget) +{ + PhoshStatusPage *self = PHOSH_STATUS_PAGE (widget); + + phosh_status_page_set_header (self, NULL); + phosh_status_page_set_content (self, NULL); + phosh_status_page_set_footer (self, NULL); + + GTK_WIDGET_CLASS (phosh_status_page_parent_class)->destroy (widget); +} + + +static void +phosh_status_page_class_init (PhoshStatusPageClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->set_property = phosh_status_page_set_property; + object_class->get_property = phosh_status_page_get_property; + + widget_class->destroy = phosh_status_page_destroy; + + /** + * PhoshStatusPage:title: + * + * The status page title + */ + props[PROP_TITLE] = + g_param_spec_string ("title", "", "", + NULL, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPage:header: + * + * An extra widget to add to end of the status page's header + */ + props[PROP_HEADER] = + g_param_spec_object ("header", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPage:content: + * + * The content of status page. + */ + props[PROP_CONTENT] = + g_param_spec_object ("content", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshStatusPage:footer: + * + * Widget displayed at the very bottom - usually a button. + */ + props[PROP_FOOTER] = + g_param_spec_object ("footer", "", "", + GTK_TYPE_WIDGET, + G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshStatusPage::done + * + * The status page should be closed + */ + signals[DONE] = g_signal_new ("done", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + g_type_ensure (PHOSH_TYPE_STATUS_PAGE_PLACEHOLDER); + gtk_widget_class_set_template_from_resource (widget_class, "/mobi/phosh/ui/status-page.ui"); + + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, content_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, footer_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, footer_separator); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, header_bin); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, title_label); + gtk_widget_class_bind_template_child_private (widget_class, PhoshStatusPage, toplevel_box); + + gtk_widget_class_set_css_name (widget_class, "phosh-status-page"); +} + + +static void +phosh_status_page_init (PhoshStatusPage *self) +{ + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +PhoshStatusPage * +phosh_status_page_new (void) +{ + return g_object_new (PHOSH_TYPE_STATUS_PAGE, NULL); +} + + +void +phosh_status_page_set_title (PhoshStatusPage *self, const char *title) +{ + PhoshStatusPagePrivate *priv; + const char *current; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + priv = phosh_status_page_get_instance_private (self); + + current = gtk_label_get_label (priv->title_label); + if (g_strcmp0 (current, title) == 0) + return; + + gtk_label_set_label (priv->title_label, title); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + + +const char * +phosh_status_page_get_title (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), NULL); + priv = phosh_status_page_get_instance_private (self); + + return gtk_label_get_label (priv->title_label); +} + +/** + * phosh_status_page_set_header: + * @self: A quick setting status page + * + * Set the header widget of the status page. See + * [property@StatusPage:header]. + */ +void +phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header_widget) +{ + PhoshStatusPagePrivate *priv; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + g_return_if_fail (header_widget == NULL || GTK_IS_WIDGET (header_widget)); + + priv = phosh_status_page_get_instance_private (self); + + if (priv->header_widget == header_widget) + return; + + if (priv->header_widget) + gtk_container_remove (GTK_CONTAINER (priv->header_bin), priv->header_widget); + + priv->header_widget = header_widget; + + if (priv->header_widget) + gtk_container_add (GTK_CONTAINER (priv->header_bin), priv->header_widget); + + gtk_widget_set_visible (GTK_WIDGET (priv->header_bin), !!header_widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HEADER]); +} + +/** + * phosh_status_page_get_header: + * @self: A quick setting status page + * + * Get the header widget of the status page + * + * Returns:(transfer none): The status page header + */ +GtkWidget * +phosh_status_page_get_header (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), NULL); + + priv = phosh_status_page_get_instance_private (self); + + return priv->header_widget; +} + +/** + * phosh_status_page_set_content: + * @self: A quick setting status page + * + * Set the content widget of the status page. See [property@StatusPage:content]. Use `NULL` to + * remove existing content. + */ +void +phosh_status_page_set_content (PhoshStatusPage *self, GtkWidget *content_widget) +{ + PhoshStatusPagePrivate *priv; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + g_return_if_fail (content_widget == NULL || GTK_IS_WIDGET (content_widget)); + + priv = phosh_status_page_get_instance_private (self); + + if (priv->content_widget == content_widget) + return; + + if (priv->content_widget) + gtk_container_remove (GTK_CONTAINER (priv->content_bin), priv->content_widget); + + priv->content_widget = content_widget; + + if (priv->content_widget) + gtk_container_add (GTK_CONTAINER (priv->content_bin), priv->content_widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONTENT]); +} + +/** + * phosh_status_page_get_content: + * @self: A quick setting status page + * + * Get the content widget of the status page + * + * Returns:(transfer none): The status page content + */ +GtkWidget * +phosh_status_page_get_content (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), NULL); + + priv = phosh_status_page_get_instance_private (self); + + return priv->content_widget; +} + +/** + * phosh_status_page_set_footer: + * @self: A quick setting status page + * + * Set the footer widget shown at the bottom of a status page + */ +void +phosh_status_page_set_footer (PhoshStatusPage *self, GtkWidget *footer_widget) +{ + PhoshStatusPagePrivate *priv; + + g_return_if_fail (PHOSH_IS_STATUS_PAGE (self)); + g_return_if_fail (footer_widget == NULL || GTK_IS_WIDGET (footer_widget)); + + priv = phosh_status_page_get_instance_private (self); + + if (priv->footer_widget == footer_widget) + return; + + if (priv->footer_widget) + gtk_container_remove (GTK_CONTAINER (priv->footer_bin), priv->footer_widget); + + priv->footer_widget = footer_widget; + + if (priv->footer_widget) + gtk_container_add (GTK_CONTAINER (priv->footer_bin), priv->footer_widget); + + gtk_widget_set_visible (GTK_WIDGET (priv->footer_separator), !!footer_widget); + gtk_widget_set_visible (GTK_WIDGET (priv->footer_bin), !!footer_widget); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_FOOTER]); +} + +/** + * phosh_status_page_get_footer: + * @self: A quick setting status page + * + * Get the footer of the status page + * + * Returns:(transfer none): The status page footer + */ +GtkWidget * +phosh_status_page_get_footer (PhoshStatusPage *self) +{ + PhoshStatusPagePrivate *priv; + + g_return_val_if_fail (PHOSH_IS_STATUS_PAGE (self), NULL); + + priv = phosh_status_page_get_instance_private (self); + + return priv->footer_widget; +} diff --git a/src/status-page.h b/src/status-page.h new file mode 100644 index 000000000..08a6629eb --- /dev/null +++ b/src/status-page.h @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2023 Tether Operations Limited + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_STATUS_PAGE phosh_status_page_get_type () +G_DECLARE_DERIVABLE_TYPE (PhoshStatusPage, phosh_status_page, PHOSH, STATUS_PAGE, GtkBin) + +struct _PhoshStatusPageClass { + GtkBinClass parent_class; + + /* Padding for future expansion */ + void (*_phosh_reserved0) (void); + void (*_phosh_reserved1) (void); + void (*_phosh_reserved2) (void); + void (*_phosh_reserved3) (void); + void (*_phosh_reserved4) (void); + void (*_phosh_reserved5) (void); + void (*_phosh_reserved6) (void); + void (*_phosh_reserved7) (void); + void (*_phosh_reserved8) (void); + void (*_phosh_reserved9) (void); +}; + +PhoshStatusPage *phosh_status_page_new (void); + +void phosh_status_page_set_title (PhoshStatusPage *self, const char *title); +const char *phosh_status_page_get_title (PhoshStatusPage *self); +void phosh_status_page_set_header (PhoshStatusPage *self, GtkWidget *header); +GtkWidget *phosh_status_page_get_header (PhoshStatusPage *self); +void phosh_status_page_set_content (PhoshStatusPage *self, GtkWidget *content); +GtkWidget *phosh_status_page_get_content (PhoshStatusPage *self); +void phosh_status_page_set_footer (PhoshStatusPage *self, GtkWidget *footer); +GtkWidget *phosh_status_page_get_footer (PhoshStatusPage *self); + +G_END_DECLS diff --git a/src/style-manager.c b/src/style-manager.c new file mode 100644 index 000000000..152700ac0 --- /dev/null +++ b/src/style-manager.c @@ -0,0 +1,283 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-style-manager" + +#include "phosh-config.h" + +#include "style-manager.h" +#include "util.h" + +#include +#include + +#define IF_KEY_ACCENT_COLOR "accent-color" +#define IF_SCHEMA_NAME "org.gnome.desktop.interface" + +/* Accent colors from gnome-shell src/st/st-theme-context.c */ +#define ACCENT_COLOR_BLUE "#3584e4" +#define ACCENT_COLOR_TEAL "#2190a4" +#define ACCENT_COLOR_GREEN "#3a944a" +#define ACCENT_COLOR_YELLOW "#c88800" +#define ACCENT_COLOR_ORANGE "#ed5b00" +#define ACCENT_COLOR_RED "#e62d42" +#define ACCENT_COLOR_PINK "#d56199" +#define ACCENT_COLOR_PURPLE "#9141ac" +#define ACCENT_COLOR_SLATE "#6f8396" +#define ACCENT_COLOR_FOREGROUND "#ffffff" + +/** + * PhoshStyleManager: + * + * The style manager is responsible for picking style sheets and + * themes and notifying other parts of the shell about changes. + */ + +enum { + PROP_0, + PROP_THEME_NAME, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshStyleManager { + GObject parent; + + char *theme_name; + GtkCssProvider *css_provider; + GtkCssProvider *accent_css_provider; + + GSettings *interface_settings; +}; +G_DEFINE_TYPE (PhoshStyleManager, phosh_style_manager, G_TYPE_OBJECT) + + +static void +on_accent_color_changed (PhoshStyleManager *self) +{ + const char *color; + g_autofree char *css = NULL; + g_autoptr (GtkCssProvider) provider = gtk_css_provider_new (); + + if (self->accent_css_provider) { + gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (self->accent_css_provider)); + } + + /* Only enable accent colors on Adwaita */ + if (g_strcmp0 (self->theme_name, "Adwaita") != 0) + return; + + switch (g_settings_get_enum (self->interface_settings, IF_KEY_ACCENT_COLOR)) { + case G_DESKTOP_ACCENT_COLOR_TEAL: + color = ACCENT_COLOR_TEAL; + break; + case G_DESKTOP_ACCENT_COLOR_GREEN: + color = ACCENT_COLOR_GREEN; + break; + case G_DESKTOP_ACCENT_COLOR_YELLOW: + color = ACCENT_COLOR_YELLOW; + break; + case G_DESKTOP_ACCENT_COLOR_ORANGE: + color = ACCENT_COLOR_ORANGE; + break; + case G_DESKTOP_ACCENT_COLOR_RED: + color = ACCENT_COLOR_RED; + break; + case G_DESKTOP_ACCENT_COLOR_PINK: + color = ACCENT_COLOR_PINK; + break; + case G_DESKTOP_ACCENT_COLOR_PURPLE: + color = ACCENT_COLOR_PURPLE; + break; + case G_DESKTOP_ACCENT_COLOR_SLATE: + color = ACCENT_COLOR_SLATE; + break; + case G_DESKTOP_ACCENT_COLOR_BLUE: + default: + color = ACCENT_COLOR_BLUE; + } + + g_debug ("Setting accent bg color to %s, accent fg color to %s", + color, ACCENT_COLOR_FOREGROUND); + + css = g_strdup_printf ("@define-color theme_selected_bg_color %s;\n" + "@define-color theme_selected_fg_color %s;", + color, ACCENT_COLOR_FOREGROUND); + gtk_css_provider_load_from_data (provider, css, -1, NULL); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1); + g_set_object (&self->accent_css_provider, provider); +} + + +static void +on_gtk_theme_name_changed (PhoshStyleManager *self, GParamSpec *pspec, GtkSettings *settings) +{ + const char *style; + g_autofree char *name = NULL; + g_autoptr (GtkCssProvider) provider = gtk_css_provider_new (); + + g_object_get (settings, "gtk-theme-name", &name, NULL); + + if (g_strcmp0 (self->theme_name, name) == 0) + return; + + self->theme_name = g_steal_pointer (&name); + g_debug ("GTK theme: %s", self->theme_name); + + if (self->css_provider) { + gtk_style_context_remove_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (self->css_provider)); + } + + style = phosh_style_manager_get_stylesheet (self->theme_name); + + gtk_css_provider_load_from_resource (provider, style); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); + g_set_object (&self->css_provider, provider); + + /* Refresh accent color */ + on_accent_color_changed (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_THEME_NAME]); +} + + +static void +phosh_style_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshStyleManager *self = PHOSH_STYLE_MANAGER (object); + + switch (property_id) { + case PROP_THEME_NAME: + g_value_set_string (value, self->theme_name); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_style_manager_dispose (GObject *object) +{ + PhoshStyleManager *self = PHOSH_STYLE_MANAGER (object); + + g_clear_pointer (&self->theme_name, g_free); + g_clear_object (&self->css_provider); + g_clear_object (&self->accent_css_provider); + + g_clear_object (&self->interface_settings); + + G_OBJECT_CLASS (phosh_style_manager_parent_class)->dispose (object); +} + + +static void +phosh_style_manager_class_init (PhoshStyleManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_style_manager_get_property; + object_class->dispose = phosh_style_manager_dispose; + + props[PROP_THEME_NAME] = + g_param_spec_string ("theme-name", "", "", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_style_manager_init (PhoshStyleManager *self) +{ + GtkSettings *gtk_settings = gtk_settings_get_default (); + + g_object_set (G_OBJECT (gtk_settings), "gtk-application-prefer-dark-theme", TRUE, NULL); + + self->interface_settings = g_settings_new (IF_SCHEMA_NAME); + + g_signal_connect_swapped (self->interface_settings, + "changed::" IF_KEY_ACCENT_COLOR, + G_CALLBACK (on_accent_color_changed), + self); + g_signal_connect_swapped (gtk_settings, + "notify::gtk-theme-name", + G_CALLBACK (on_gtk_theme_name_changed), + self); + on_gtk_theme_name_changed (self, NULL, gtk_settings); +} + + +PhoshStyleManager * +phosh_style_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_STYLE_MANAGER, NULL); +} + +/** + * phosh_style_manager_get_stylesheet: + * @theme_name: A theme name + * + * Get the proper style sheet based on the given theme name + */ +const char * +phosh_style_manager_get_stylesheet (const char *theme_name) +{ + const char *style; + + if (g_strcmp0 (theme_name, "HighContrast") == 0) + style = "/mobi/phosh/stylesheet/adwaita-hc-light.css"; + else + style = "/mobi/phosh/stylesheet/adwaita-dark.css"; + + return style; +} + +/** + * phosh_style_manager_get_theme_name: + * @self; The style manager + * + * Get the current theme name + * + * Returns: The theme name + */ +const char * +phosh_style_manager_get_theme_name (PhoshStyleManager *self) +{ + g_return_val_if_fail (PHOSH_IS_STYLE_MANAGER (self), NULL); + + return self->theme_name; +} + +/** + * phosh_style_manager_is_high_contrast: + * @self; The style manager + * + * Get whether the current theme is a high contrast theme + * + * Returns: `True` if current theme is high contrast + */ +gboolean +phosh_style_manager_is_high_contrast (PhoshStyleManager *self) +{ + g_return_val_if_fail (PHOSH_IS_STYLE_MANAGER (self), FALSE); + + return g_strcmp0 (self->theme_name, "HighContrast") == 0; +} diff --git a/src/style-manager.h b/src/style-manager.h new file mode 100644 index 000000000..5f0d9e090 --- /dev/null +++ b/src/style-manager.h @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Phosh Developers + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_STYLE_MANAGER (phosh_style_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshStyleManager, phosh_style_manager, PHOSH, STYLE_MANAGER, GObject) + +PhoshStyleManager *phosh_style_manager_new (void); +const char *phosh_style_manager_get_theme_name (PhoshStyleManager *self); +gboolean phosh_style_manager_is_high_contrast (PhoshStyleManager *self); + +const char *phosh_style_manager_get_stylesheet (const char *theme_name); + +G_END_DECLS diff --git a/src/stylesheet/adwaita-dark.css b/src/stylesheet/adwaita-dark.css new file mode 100644 index 000000000..c01e50252 --- /dev/null +++ b/src/stylesheet/adwaita-dark.css @@ -0,0 +1,22 @@ +/* Adwaita dark theme variant */ + +@define-color phosh_fg_color white; +@define-color phosh_bg_color black; + +@define-color phosh_borders_color alpha(@phosh_fg_color,.1); + +@define-color phosh_notification_bg_color #282828; +@define-color phosh_action_bg_color #474747; +@define-color phosh_activity_bg_color alpha(@phosh_notification_bg_color,.7); +@define-color phosh_splash_bg_color #f6f5f4; +@define-color phosh_splash_fg_color #282828; + +/* Button colors */ +@define-color phosh_button_bg_color #282828; +@define-color phosh_button_hover_bg_color shade(@phosh_button_bg_color, 1.14); +@define-color phosh_button_active_bg_color shade(@phosh_button_bg_color, 1.5); + +@define-color phosh_emergency_button_bg_color #e01b24; +@define-color phosh_emergency_button_fg_color #ffffff; + +@import url("resource:///mobi/phosh/stylesheet/common.css"); diff --git a/src/stylesheet/adwaita-hc-light.css b/src/stylesheet/adwaita-hc-light.css new file mode 100644 index 000000000..1d4abc03b --- /dev/null +++ b/src/stylesheet/adwaita-hc-light.css @@ -0,0 +1,69 @@ +/* HighContrast theme variant */ + +@define-color phosh_fg_color black; +@define-color phosh_bg_color white; + +@define-color phosh_borders_color alpha(@phosh_fg_color,.1); + +@define-color phosh_activity_bg_color #e5e5e5; +@define-color phosh_notification_bg_color #e0e0e0; +@define-color phosh_action_bg_color #787878; +@define-color phosh_splash_bg_color @theme_bg_color; +@define-color phosh_splash_fg_color @theme_fg_color; + +/* Button Colors */ +@define-color phosh_button_bg_color #e0e0e0; +@define-color phosh_button_hover_bg_color shade(@phosh_button_bg_color, 1.05); +@define-color phosh_button_active_bg_color shade(@phosh_button_bg_color, 1.1); + +@define-color phosh_emergency_button_bg_color #e01b24; +@define-color phosh_emergency_button_fg_color #ffffff; + +@import url("resource:///mobi/phosh/stylesheet/common.css"); + +#top-bar, #home-bar { + box-shadow: inset 0 0 0 1px @phosh_borders_color; +} + +.phosh-quick-setting, +button { + box-shadow: inset 0 0 0 2px @phosh_borders_color; +} + +button:hover { + box-shadow: inset 0 0 0 2px @phosh_fg_color; +} + +button:disabled { + box-shadow: inset 0 0 0 2px alpha(@phosh_borders_color, 0.5); +} + +.emergency-button:hover { + box-shadow: inset 0 0 2px 0 shade(@phosh_emergency_button_bg_color, .6); +} +.emergency-button:focus { + box-shadow: inset 0 0 2px 0 shade(@phosh_emergency_button_bg_color, .4); +} + +#top-bar-bin:not(.p-solid) #top-bar label, +#phosh-lockscreen-clock, +#phosh-lockscreen-date, +.phosh-lockscreen-arrow + label, +.phosh-lockscreen-pin, +.phosh-lockscreen-pin:disabled, +.phosh-lockscreen-pin:focus, +.phosh-lockscreen-unlocker > label, +phosh-keypad label.digit, +phosh-lockscreen cui-call-display label + { + text-shadow: none; +} + +#top-bar-bin:not(.p-solid) #top-bar image, +phosh-keypad image { + -gtk-icon-shadow: none; +} + +.phosh-search-bar { + background-color: @phosh_button_bg_color; +} diff --git a/src/stylesheet/common.css b/src/stylesheet/common.css new file mode 100644 index 000000000..426d63d3b --- /dev/null +++ b/src/stylesheet/common.css @@ -0,0 +1,1286 @@ +/* + * Top panel and bar + */ +phosh-top-panel { + font-size: 15px; + background: none; + color: @phosh_fg_color; +} + +phosh-top-panel-bg { + background: @phosh_bg_color; +} + +#top-bar-bin { + transition: background-color 200ms ease; +} + +#top-bar-bin:not(.p-solid) #top-bar label { + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +#top-bar-bin:not(.p-solid) #top-bar image { + -gtk-icon-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +#top-bar { + padding-top: 4px; + padding-bottom: 4px; +} + +.phosh-wwan-indicator, +phosh-top-panel .indicators { + font-size: 13px; + font-weight: 800; + font-feature-settings: "tnum"; +} + +.phosh-topbar-clock { + font-weight: bold; + font-feature-settings: "tnum"; +} + +phosh-top-panel .phosh-topbar-clock { + font-size: 16px; +} + +/* When moved to the left because of a cutout */ +phosh-top-panel .phosh-topbar-clock.left { + padding-right: 6px; +} + +/* When moved to the right because of a cutout */ +phosh-top-panel .phosh-topbar-clock.right { + padding-left: 6px; +} + +phosh-top-panel .phosh-topbar-date { + font-size: 14px; +} + +.phosh-clock-box { + padding: 16px; +} + +.phosh-topbar-button { + padding: 0 12px; + border-radius: 99px; +} + +/* + * Settings menu + */ + +widget.phosh-settings-menu > scrolledwindow { + background-color: @phosh_bg_color; + border-radius: 24px; + margin-left: 6px; + margin-right: 6px; +} + +.phosh-settings-menu { + background-color: @phosh_bg_color; + border-bottom-right-radius: 12px; + border-bottom-left-radius: 12px; +} + +.phosh-settings-menu button.circular:not(:hover):not(:active) { + border: 2px solid @theme_bg_color; +} + +.phosh-settings-menu > scrolledwindow > viewport { + padding: 0 12px; +} + +.phosh-settings-menu separator { + background: @phosh_borders_color; +} + +phosh-quick-setting { + border-radius: 99px; + border: 0; + font-weight: bold; + font-size: 10pt; + background-color: @phosh_button_bg_color; +} + +phosh-quick-setting button:hover { + background-color: shade(@phosh_button_bg_color, 1.14); +} + +phosh-quick-setting:checked button:hover { + background-color: shade(@theme_selected_bg_color, 1.14); + color: @theme_selected_fg_color; +} + +phosh-quick-setting:checked { + background-color: @theme_selected_bg_color; + color: @theme_selected_fg_color; +} + +phosh-quick-setting:disabled { + opacity: 0.5; + background-color: @phosh_button_bg_color; +} + +phosh-quick-setting button:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + outline: none; +} + +phosh-quick-setting:checked button:focus { + box-shadow: inset 0 0 0 2px alpha(@theme_fg_color,.3); + outline: none; +} + +phosh-quick-setting button:first-child { + border-radius: 99px 0px 0px 99px; +} + +phosh-quick-setting button:only-child { + border-radius: 99px; +} + +phosh-quick-setting button:last-child:not(:only-child) { + border-radius: 0px 99px 99px 0px; +} + +phosh-quick-setting button { + background: none; + background-color: transparent; + border: none; + box-shadow: none; + -gtk-icon-shadow: none; + padding: 0; + text-shadow: none; + padding: 8px 12px; +} + +phosh-brightness-settings scale { + padding-right: 4px; +} + +/* + * Volume slider in settings + */ +phosh-channel-bar image { + -gtk-icon-style: symbolic; +} + +phosh-channel-bar scale { + padding-right: 4px; +} + +/* Toggle buttions to open details in settings */ +button.phosh-settings-details, +button.phosh-settings-details:focus { + background: none; +} + +/* Listboxes in settings (e.g. audio details) */ +.phosh-settings-list-box { + border-radius: 18px; + padding: 12px; + border: none; + background-color: @phosh_notification_bg_color; +} + +.phosh-settings-list-box list { + background-color: transparent; +} + +.phosh-settings-list-box list row { + border-radius: 8px; +} + +.phosh-settings-list-box list row:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background: mix(@phosh_button_bg_color, @theme_selected_bg_color, 0.1); + outline-style: none; + transition: none; +} + +.phosh-settings-list-box list row image { + -gtk-icon-style: symbolic; +} + +/* Status pages of quick settings (e.g. audio wifi or bt status pages) */ +.phosh-status-page { + border-radius: 18px; + padding: 12px 12px 6px; + border: none; + background-color: @phosh_notification_bg_color; +} + +.phosh-status-page list row:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background: mix(@phosh_button_bg_color, @theme_selected_bg_color, 0.1); + outline-style: none; + transition: none; +} + +.phosh-status-page list row image { + -gtk-icon-style: symbolic; +} + +phosh-status-page-placeholder > box > label.title { + font-weight: 400; + font-size: 16px; +} + +/* + * Audio devices listbox + */ +.phosh-gear-button { + border-radius: 99px; +} + +/* + * System menu + */ +#system-menu button { + padding: 8px 16px; + border-radius: 99px; + font-weight: bold; + font-size: 90%; +} + +/* + * Settings gear button + */ +#settings-gear { + background: none; +} + + +/* + * phosh-home + */ + +#powerbar { + background: -gtk-recolor(url('resource:///mobi/phosh/icons/scalable/status/input-powerbar-symbolic.svg')); + background-position: center; + background-size: 150px 15px; + background-repeat: no-repeat; + transition: 400ms ease; +} + +.p-active #powerbar { + opacity: 0.5; +} + +.p-failed #powerbar { + background-size: 150px 15px; + transition: 300ms ease; + animation: error-shake 1.5s ease; + animation-iteration-count: 1; +} + +@keyframes error-shake { + 0% {background-position: center; opacity: .5;} + 10% {background-position: center; opacity: .7;} + 15% {background-position: left; opacity: 1;} + 30% {background-position: right;} + 45% {background-position: left;} + 60% {background-position: right;} + 80% {background-position: center;} + 100% {background-position: center;} +} + +/* + * Overview (app grid with favorites and activities) + */ + +phosh-activity > widget > button { + background: none; + transition: none; + border: none; + border-radius: 6px; + color: inherit; + /* don't add padding here - it will break PhoshActivity's sizing */ + /* use GtkDrawingArea's margin instead */ + padding: 0px; +} + +phosh-activity > widget > button:hover { + background: none; +} + +phosh-activity > widget > button:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + outline: none; +} + +phosh-activity box button { + background: @phosh_bg_color; + border-radius: 0; + /* box-shadow: 0 2px 2px rgba(0,0,0,0.4), 0 3px 16px rgba(0,0,0,0.3); */ +} + +phosh-activity.phosh-empty.light { + color: @phosh_splash_fg_color; +} + +phosh-activity.phosh-empty.light .phosh-drawingarea { + color: @phosh_splash_fg_color; + background-color: @phosh_splash_bg_color; +} + +phosh-activity.phosh-empty .phosh-drawingarea { + border-radius: 8px; + background-color: @theme_bg_color; +} + +phosh-activity.phosh-maximized .phosh-drawingarea { + border-radius: 8px; + background-color: @phosh_activity_bg_color; +} + +phosh-home { + background: none; + color: @phosh_fg_color; +} + +.phosh-activity-close-button { + min-width: 32px; + min-height: 32px; + padding: 8px; + margin: 10px; + border-radius: 50%; +} + +.phosh-overview { + background: none; + color: @phosh_fg_color; +} + +.phosh-favorite { + background: none; + border: none; + padding: 0; +} + +.phosh-favorite:hover { + -gtk-icon-effect: none; +} + +.phosh-favorite:active { + -gtk-icon-effect: highlight; +} + +/* Search Bar */ +.phosh-search-bar-box { + margin: 6px 16px; +} + +.phosh-search-bar { + border-radius: 9999px; + padding: 3px 18px 3px 14px; + border: 1px solid transparent; + background-color: alpha(@phosh_bg_color,.6); + color: inherit; +} + +.phosh-search-bar > image { + color: inherit; + opacity: 0.7; +} + +.phosh-search-bar:focus { + box-shadow: inset 0 0 0 1px @theme_selected_bg_color; + border: 1px solid @theme_selected_bg_color; +} + +phosh-app-grid { + background: none; + color: @phosh_fg_color; +} + +phosh-app-grid separator { + background: @phosh_borders_color; + min-height: 2px; + border-radius: 1px; + margin: 0 12px; +} + +phosh-app-grid-button button { + font-size: 0.8rem; + border-radius: 9px; + margin: 0; + padding: 0; + background: none; +} + +phosh-home:not(.p-solid) phosh-app-grid label { + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +phosh-app-grid-button button:active, +phosh-app-grid-button button:hover:active, +phosh-app-grid-button button:focus { + background: alpha(@phosh_bg_color,.4); +} + +phosh-app-grid-folder-button button { + font-size: 0.8rem; + border-radius: 9px; + margin: 0; + padding: 0; + background: alpha(@phosh_bg_color,.4); +} + +phosh-app-grid-folder-button button:active, +phosh-app-grid-folder-button button:hover, +phosh-app-grid-folder-button button:hover:active, +phosh-app-grid-folder-button button:focus { + background: alpha(@phosh_bg_color,.6); +} + +.search-active phosh-app-grid-button:first-child button { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background: alpha(@phosh_bg_color,.4); +} + +#phosh-filter-adaptive-btn { + border-radius: 9999px; + padding: 6px 24px; + margin: 24px 0; +} + +.phosh-app-grid-folder-page-title-box { + margin: 6px 16px; +} + +/* + * Lock screen + */ + +phosh-lockscreen { + background-color: transparent; + color: @phosh_fg_color; +} + +phosh-lockscreen-bg { + background-color: @phosh_bg_color; + color: @phosh_fg_color; +} + +.phosh-lockshield { + background-color: @phosh_bg_color; + color: @phosh_fg_color; +} + +/* Clock */ +#phosh-lockscreen-clock { + font-weight: 300; + font-size: 84px; + font-feature-settings: "tnum"; + margin: 12px 0; + transition: 200ms ease; + padding-top: 40px; + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +#phosh-lockscreen-date { + font-weight: 400; + font-size: 18px; + margin: 6px 0; + transition: 200ms ease; + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +.p-small #phosh-lockscreen-clock { + padding-top: 0px; + font-size: 72px; + transition: 200ms ease; +} +.p-small #phosh-lockscreen-date { + font-size: 16px; + margin-top: 0; + transition: 200ms ease; +} + +/* Lockscreen Arrow */ +.phosh-lockscreen-arrow { + transition: 400ms linear; + animation: pulsate 1.8s ease-out; + animation-iteration-count: 15; +} + +.phosh-lockscreen-arrow + label { + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +.phosh-lockscreen-status-icons { + padding: 6px; +} + +.phosh-lockscreen-pin, +.phosh-lockscreen-pin:disabled, +.phosh-lockscreen-pin:focus { + font-size: 20px; + padding: 6px; + border-radius: 12px; + color: inherit; + border: none; + box-shadow:none; + outline: none; + caret-color: transparent; + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +.phosh-lockscreen-unlocker > label { + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +@keyframes pulsate { + 0% {-gtk-icon-transform: translateY(0); opacity: 0.7;} + 35% {-gtk-icon-transform: translateY(-14px); opacity: 1;} + 100% {-gtk-icon-transform: translateY(0); opacity: 0.7;} +} + +/* Lockscreen keypad */ +phosh-keypad label.digit { + font-size: 200%; + font-weight: bold; + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +phosh-keypad button { + border-radius: 9999px; + font-size: 16px; + font-weight: bold; + padding: 16px; + background: none; +} + +phosh-keypad image { + -gtk-icon-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +phosh-keypad button:active label, +phosh-keypad button:active image { + text-shadow: none; + -gtk-icon-shadow: none; +} + +phosh-keypad button:active, +phosh-keypad button:hover, +phosh-keypad button:hover:active, +phosh-keypad button:focus { + background: alpha(@phosh_fg_color, 0.3); +} + +phosh-lockscreen .text-only-button { + font-size: 16px; + font-weight: bold; + padding: 16px 36px; +} + +phosh-lockscreen cui-call-display actionbar { + background-color: @phosh_bg_color; +} + +phosh-lockscreen cui-call-display label { + text-shadow: 1px 1px 1.1px rgba(0, 0, 0, 0.3); +} + +phosh-lockscreen .phosh-notification-tray { + margin: 0 6px; + transition: 200ms ease; +} + +phosh-lockscreen phosh-media-player { + margin: 0 8px; +} + +phosh-lockscreen .phosh-notification-tray separator { + background: @phosh_borders_color; +} + +phosh-lockscreen .phosh-notification-tray row:focus { + border-radius: 14px; + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + outline: none; +} + +/* Widget box */ +phosh-lockscreen phosh-widget-box { + margin-left: 6px; + margin-right: 6px; +} + +/* + * System modal dialogs (polkit, gcr, ...) + */ + +.phosh-system-modal { + background-color: alpha(@phosh_bg_color, 0.8); +} + +.phosh-system-modal-dialog { + background-color: @theme_bg_color; + border: 1px solid @phosh_borders_color; + border-radius: 12px; + box-shadow: 0 2px 4px 0 rgba(0,0,0,.2); + padding: 0px; + padding-top: 18px; + margin: 12px; +} + +.phosh-system-modal-dialog-buttons label { + margin: 12px; +} + +.phosh-system-modal-dialog-buttons button { + padding: 0; + margin: 0; + border: 1px solid @phosh_borders_color; + border-left-width: 0; + border-bottom-width: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.phosh-system-modal-dialog-buttons button:first-child { + border-bottom-left-radius: 11px; + border-bottom-right-radius: 0; +} + +.phosh-system-modal-dialog-buttons button:last-child { + border-bottom-left-radius: 0; + border-bottom-right-radius: 11px; + border-right: none; +} + +.phosh-system-modal-dialog-buttons button:only-child { + border-bottom-left-radius: 11px; + border-bottom-right-radius: 11px; +} + +.phosh-system-modal-dialog-content { + padding-left: 18px; + padding-right: 18px; +} + +.phosh-system-modal-dialog-title { + font-weight: 800; + font-size: 120%; +} + +.phosh-end-session-dialog .phosh-end-session-subtitle { + font-feature-settings: "tnum"; +} + +.phosh-end-session-dialog .phosh-end-session-warning { + color: #ba5645; + font-weight: bold; +} + +.phosh-end-session-dialog list { + border-radius: 12px; +} + +.phosh-end-session-dialog list row { + margin-left: 6px; + margin-right: 6px; +} + +.phosh-end-session-dialog list row:first-child { + margin-top: 6px; +} + +.phosh-end-session-dialog list row:last-child { + margin-bottom: 6px; +} + +.phosh-end-session-dialog list row box box.vertical label:first-child { + font-weight: bold; +} + +/* Don't dim list of inhibited applications */ +.phosh-end-session-dialog list * { + color: @theme_text_color; + -gtk-icon-effect: none; +} + +phosh-gtk-mount-prompt .phosh-system-modal-dialog-content > image { + -gtk-icon-style: symbolic; +} + +/* + * Notifications + */ + +phosh-notification-content { + background: transparent; + color: @phosh_fg_color; + padding: 0; + margin: 0; +} + +phosh-notification-content .message-area { + padding: 12px 12px 9px 12px; +} + +.phosh-notification-body:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + outline-style: none; +} + +phosh-notification-content .message-area image { + border-radius: 50%; +} + +/* Action Area (buttons) */ +phosh-notification-content .actions-area { + padding: 0; + border: none; +} + +phosh-notification-content .actions-area button { + background: @phosh_action_bg_color; + border: 1px solid @phosh_borders_color; + border-radius: 0; + font-weight: bold; + padding: 8px 12px; + box-shadow: inset -1px 0 0 0 @phosh_borders_color; +} + +phosh-notification-content .actions-area button:only-child { + border-bottom-left-radius: 10px; + border-bottom-right-radius: 10px; + box-shadow: none; +} + +phosh-notification-content .actions-area button:first-child { + border-bottom-left-radius: 10px; +} +phosh-notification-content .actions-area button:last-child { + border-bottom-right-radius: 10px; + box-shadow: none; +} + +phosh-notification-content .actions-area button:hover { + background-color: rgba(255,255,255,.07); +} + +phosh-notification-content .actions-area button:active { + background-color: rgba(255,255,255,.14); +} + +phosh-notification-content .actions-area button:focus, +phosh-notification-content .actions-area button:only-child:focus, +phosh-notification-content .actions-area button:last-child:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background-color: alpha(@theme_selected_bg_color, 0.2); +} + +phosh-notification-frame { + margin: 0; + padding: 0; + color: @phosh_fg_color; +} + +phosh-notification-frame list { + background: transparent; +} + +phosh-notification-frame .header-area { + padding: 12px 12px 0 12px; +} + +phosh-notification-frame .header-area image { + -gtk-icon-style: symbolic; +} + +phosh-notification-frame .notification-container { + border-radius: 12px; + border: none; + background-color: @phosh_notification_bg_color; +} + +/* Banner */ +phosh-notification-banner { + background: none; + border: none; + color: inherit; +} + +phosh-notification-banner > phosh-notification-frame { + padding: 0; +} + +phosh-notification-banner .notification-container { + margin: 6px; + box-shadow: 0px 1px 4px 0px rgba(0, 0, 0, 0.33), + 0px 2px 4px 2px rgba(0, 0, 0, 0.07); +} + +/* Notification Tray */ +.phosh-notification-tray { + padding-bottom: 12px; +} + +.phosh-notification-tray, +.phosh-notification-tray list { + background: transparent; +} + +.notification-container *:focus, +.phosh-notification-tray list row:focus { + outline-style: none; +} + +.phosh-notification-tray list row .notification-container { + margin-top: 12px; +} + +.phosh-notifications-header { + padding:0; +} + +/* Clear notifications button */ +#phosh-notifications-clear-all-btn { + border-radius: 99px; +} + +/* Call notifications */ +phosh-call-notification { + padding: 12px; + color: @phosh_fg_color; + border-radius: 12px; + border: none; + background-color: @phosh_notification_bg_color; +} + +/* + * Output fader + */ + +@keyframes phosh-fader-default-keyframe { + from {background:rgba(0, 0, 0, 0);} + to {background:rgba(0, 0, 0, 1);} +} + +.phosh-fader-default-fade { + animation-name: phosh-fader-default-keyframe; + animation-duration: 2s; + animation-timing-function: linear; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +/* Fader used for proximity dimming */ +.phosh-fader-proximity-fade { + animation-name: phosh-fader-default-keyframe; + animation-duration: 200ms; + animation-timing-function: linear; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +/* Fader used for screenshot flash */ +@keyframes phosh-fader-flash-keyframe { + from {background:rgba(255, 255, 255, 1);} + to {background:rgba(255, 255, 255, 0);} +} + +.phosh-fader-flash-fade { + animation-name: phosh-fader-flash-keyframe; + animation-duration: 500ms; + animation-timing-function: linear; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +.phosh-fader-screenshot-opaque { + background: rgba(255, 255, 255, 0); +} + +/* Theme change fader */ +@keyframes phosh-fader-theme-to-hc-keyframe { + from {background:rgba(0, 0, 0, 0);} + to {background:rgba(255, 255, 255, 1.0);} +} + +.phosh-fader-theme-to-hc { + animation-name: phosh-fader-theme-to-hc-keyframe; + animation-duration: 100ms; + animation-timing-function: ease-out; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +@keyframes phosh-fader-theme-from-hc-keyframe { + from {background:rgba(255, 255, 255, 0);} + to {background:rgba(0, 0, 0, 1.0);} +} + +.phosh-fader-theme-from-hc { + animation-name: phosh-fader-theme-from-hc-keyframe; + animation-duration: 100ms; + animation-timing-function: ease-out; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +/* + * Media player + */ + +phosh-media-player { + background: @phosh_notification_bg_color; + color: @phosh_fg_color; + border-radius: 12px; + border:none; + box-shadow: none; + padding: 6px; + padding-bottom: 0; +} + +phosh-media-player button { + background: none; +} + +phosh-media-player button:focus { + background-color: alpha(@theme_selected_bg_color, .2); +} + +phosh-media-player button:hover { + background-color: rgba(255,255,255,.1); +} + +phosh-media-player button:active { + background-color: rgba(255,255,255,.2); +} + +phosh-media-player .media-player-details { + border-radius: 8px; + padding-left: 4px; +} + +/* The details button should not look inactive even when disabled */ +phosh-media-player .media-player-details:disabled { + color: @theme_fg_color; + opacity: 1.0; + -gtk-icon-effect: none; +} + +phosh-media-player .phosh-media-player-art { + margin: 0; + padding: 0; + border-radius: 3px; +} + +phosh-media-player .progress-bar { + font-size: small; +} + +phosh-media-player > box > button { + border-radius: 9999px; + margin: 2px; + padding: 8px; +} + +/* + * OSD + */ + +#osd-bubble { + border-radius: 32px; + box-shadow: 0 2px 2px rgba(0,0,0,0.2), + 0 3px 16px rgba(0,0,0,0.1); + background-color: shade(@phosh_notification_bg_color, 1.5); + color: @phosh_fg_color; + text-shadow: none; + -gtk-icon-shadow: none; +} + +#osd-label { + font-size: 9pt; +} + +phosh-osd-window.phosh-system-modal { + background: none; +} + +phosh-osd-window levelbar block { + min-height:4px; + box-shadow: none; + border: none; + border-radius: 2px; +} + +phosh-osd-window levelbar block.low { + background-color: @phosh_fg_color; +} + +phosh-osd-window levelbar block.high { + background-color: @phosh_fg_color; +} + +phosh-osd-window levelbar block.full { + background-color: @phosh_fg_color; +} + +phosh-osd-window levelbar trough { + border: none; + padding: 0; + box-shadow: none; + background-color: alpha(@phosh_fg_color, 0.2); + border-radius: 2px; +} + +/* + * Splash + */ + +phosh-splash.light { + color: @phosh_splash_fg_color; + background-color: @phosh_splash_bg_color; +} + +phosh-splash.dark { + /* phosh prefers dark, nothing to do */ +} + +.phosh-dropicon-shadow { + -gtk-icon-shadow: 0 1px 2px rgba(0, 0, 0, 0.4), 0 1px 8px rgba(0, 0, 0, 0.2); +} + +phosh-splash spinner { + -gtk-icon-source: -gtk-icontheme('splash-process-working-symbolic'); +} + +/* + * Power button menu + */ + +/* Special style for the dialog */ +phosh-power-menu .phosh-system-modal-dialog { + border-radius:32px; + padding-bottom: 4px; + margin: 24px; +} + +phosh-power-menu grid button { + min-height: 80px; + min-width: 64px; + border-radius: 24px; + font-weight: bold; +} + +phosh-power-menu button image { + opacity: 0.8; +} + +/* Some hardcoded positioning, which is not ideal */ +phosh-power-menu grid { + margin-top: -16px; +} + +phosh-power-menu #close_button { + margin-top: -36px; + margin-right: -36px; +} + +phosh-power-menu #close_button button { + border-radius: 99px; + padding: 8px; +} + +phosh-power-menu .phosh-dialog-close-button { + box-shadow: 0 2px 4px 0 rgba(0,0,0,.2); +} + +/* Emergency button in the power menu */ +.emergency-button { + background: @phosh_emergency_button_bg_color; + color: @phosh_emergency_button_fg_color; +} + +.emergency-button:focus, +.emergency-button:focus:hover, +.emergency-button:hover { + background: shade(@phosh_emergency_button_bg_color, 1.2); +} + +.emergency-button:focus { + box-shadow: inset 0 0 2px 0 shade(@phosh_emergency_button_bg_color, 2); +} + +.emergency-button:active { + background: shade(@phosh_emergency_button_bg_color, 1.5); +} + +/* + * Emergency call menu + */ + +phosh-emergency-menu .emergency-button { + font-size: 12px; + padding: 4px 12px; +} + +/* + * Common style classes + */ + +/* Use a solid background, useful when one needs to toggle between transparent and solid */ +.p-solid { + background-color: @phosh_bg_color; +} + +/* + * Background + */ +phosh-background { + background: @phosh_bg_color; + border-width: 0px; + border-radius: 0px; +} + +/* + * GTK Overrides + */ + +/* Remove this from all elements */ +* { + text-shadow: none; + -gtk-icon-shadow: none; +} + +/* Buttons */ + +button { + background: @phosh_button_bg_color; + border: none; + box-shadow: none; + color: inherit; +} + +button:hover { + background: @phosh_button_hover_bg_color; +} + +button:disabled { + opacity: 0.5; + box-shadow: none; +} + +button:active, +button:hover:active { + background: @phosh_button_active_bg_color; +} + +button:focus { + box-shadow: inset 0 0 0 2px @theme_selected_bg_color; + background: mix(@phosh_button_bg_color, @theme_selected_bg_color, 0.1); + outline-style: none; + transition: none; +} + +button:focus:hover { + background: mix(@phosh_button_hover_bg_color, @theme_selected_bg_color, 0.1); +} + +button:focus:active { + background: mix(@phosh_button_active_bg_color, @theme_selected_bg_color, 0.1); +} + +button.suggested-action { + background-color: @theme_selected_bg_color; + color: @theme_selected_fg_color; +} + +button.suggested-action:focus { + box-shadow: inset 0 0 0 2px alpha(@theme_selected_fg_color, 0.3); +} + +/* FIXME: an override to prevent double highlighting on some buttons */ +phosh-quick-setting button:focus, +phosh-app-grid-button button:hover, +phosh-keypad button:hover, +phosh-media-player button:hover { + background: none; +} +/* End FIXME */ + +row:selected { + background-color: @theme_selected_bg_color; +} + +/* Scrollbars */ +scrollbar { + background-color: transparent; + border: none; +} + +/* Sliders */ +scale trough { + padding: 1px; + border-radius: 2px; + border: none; + background: alpha(@phosh_fg_color, 0.3); + outline-style: solid; + outline-width: 1px; + outline-color: @theme_selected_bg_color; + outline-offset: 2px; +} + +scale trough > highlight { + padding: 2px; + border: none; +} + +scale trough slider { + border-width: 1px; + border-style: solid; + border-color: shade(@phosh_fg_color, 0.9); + background: shade(@phosh_fg_color, 0.9); +} + +scale trough slider:hover { + border-color: @phosh_fg_color; + background: @phosh_fg_color; +} + +scale trough slider:active { + border-color: shade(@phosh_fg_color, 0.9); + background: shade(@phosh_fg_color, 0.9); +} + +scale highlight { + background-color: @theme_selected_bg_color; +} + +/* Switches */ +switch:not(:disabled):checked { + background-color: @theme_selected_bg_color; +} + +/* Entries */ +entry:focus { + box-shadow: inset 0 0 0 1px @theme_selected_bg_color; +} + +/* Progress bars */ +progressbar progress { + background-color: @theme_selected_bg_color; +} + +/* Levelbars */ +levelbar block.filled { + background-color: @theme_selected_bg_color; + border-color: @theme_selected_bg_color; +} + +levelbar trough { + border-width: 0px; +} + +/* Calendar widgets */ +calendar:selected { + background-color: @theme_selected_bg_color; +} diff --git a/src/suspend-manager.c b/src/suspend-manager.c new file mode 100644 index 000000000..3fbb403ff --- /dev/null +++ b/src/suspend-manager.c @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2022-2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-suspend-manager" + +#include "phosh-config.h" + +#include "login1-manager-dbus.h" +#include "util.h" +#include "shell-priv.h" +#include "suspend-manager.h" +#include "wifi-manager.h" + +#include + +#define LOGIN_BUS_NAME "org.freedesktop.login1" +#define LOGIN_OBJECT_PATH "/org/freedesktop/login1" + +/** + * PhoshSuspendManager: + * + * Manages suspend and inhibit's suspend when not useful. + */ +struct _PhoshSuspendManager { + PhoshManager parent; + + PhoshDBusLoginManager *logind_manager_proxy; + GHashTable *inhibitors; /* key: name, value: fd */ + + GCancellable *cancel; +}; +G_DEFINE_TYPE (PhoshSuspendManager, phosh_suspend_manager, PHOSH_TYPE_MANAGER) + + +static void +inhibit_suspend (PhoshSuspendManager *self, const char *what, const char *reason) +{ + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + guint cookie; + + cookie = phosh_session_manager_inhibit (sm, PHOSH_SESSION_INHIBIT_SUSPEND, reason); + g_hash_table_insert (self->inhibitors, g_strdup (what), GUINT_TO_POINTER (cookie)); +} + + +static void +uninhibit_suspend (PhoshSuspendManager *self, const char *what) +{ + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + gpointer value; + + value = g_hash_table_lookup (self->inhibitors, what); + if (value) + phosh_session_manager_uninhibit (sm, GPOINTER_TO_UINT (value)); + + g_hash_table_remove (self->inhibitors, "wifi-hotspot"); +} + + +static void +on_is_hotspot_master_changed (PhoshSuspendManager *self, + GParamSpec *pspec, + PhoshWifiManager *wifi_manager) +{ + gboolean is_hotspot_master; + + g_return_if_fail (PHOSH_IS_SUSPEND_MANAGER (self)); + g_return_if_fail (PHOSH_IS_WIFI_MANAGER (wifi_manager)); + + is_hotspot_master = phosh_wifi_manager_is_hotspot_master (wifi_manager); + + if (is_hotspot_master) { + inhibit_suspend (self, "wifi-hotspot", "Wi-Fi hotspot active"); + } else { + g_debug ("Clearing Wi-Fi hotspot suspend inhibit"); + uninhibit_suspend (self, "wifi-hotspot"); + } +} + + + +static void +uninhibit (gpointer pointer) +{ + int fd = GPOINTER_TO_INT (pointer); + + if (fd >= 0) + close (fd); +} + + +static void +on_logind_manager_proxy_new_for_bus_finish (GObject *source_object, + GAsyncResult *res, + PhoshSuspendManager *self) +{ + g_autoptr (GError) err = NULL; + PhoshWifiManager *wifi_manager; + PhoshDBusLoginManager *proxy; + + proxy = phosh_dbus_login_manager_proxy_new_for_bus_finish (res, &err); + if (proxy == NULL) { + phosh_dbus_service_error_warn (err, "Failed to get login1 manager proxy"); + return; + } + + g_return_if_fail (PHOSH_IS_SUSPEND_MANAGER (self)); + g_debug ("Connected to " LOGIN_OBJECT_PATH); + self->logind_manager_proxy = proxy; + + wifi_manager = phosh_shell_get_wifi_manager (phosh_shell_get_default()); + g_signal_connect_swapped (wifi_manager, + "notify::is-hotspot-master", + G_CALLBACK (on_is_hotspot_master_changed), + self); + on_is_hotspot_master_changed (self, NULL, wifi_manager); +} + + +static void +phosh_suspend_manager_idle_init (PhoshManager *manager) +{ + PhoshSuspendManager *self = PHOSH_SUSPEND_MANAGER (manager); + + phosh_dbus_login_manager_proxy_new_for_bus ( + G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + LOGIN_BUS_NAME, + LOGIN_OBJECT_PATH, + self->cancel, + (GAsyncReadyCallback) on_logind_manager_proxy_new_for_bus_finish, + self); +} + + +static void +phosh_suspend_manager_finalize (GObject *object) +{ + PhoshSuspendManager *self = PHOSH_SUSPEND_MANAGER(object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + g_clear_object (&self->logind_manager_proxy); + g_clear_pointer (&self->inhibitors, g_hash_table_destroy); + + G_OBJECT_CLASS (phosh_suspend_manager_parent_class)->finalize (object); +} + + +static void +phosh_suspend_manager_class_init (PhoshSuspendManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->finalize = phosh_suspend_manager_finalize; + manager_class->idle_init = phosh_suspend_manager_idle_init; +} + + +static void +on_suspend_finished (PhoshDBusLoginManager *proxy, + GAsyncResult *res, + PhoshSessionManager *self) +{ + g_autoptr (GError) err = NULL; + + if (!phosh_dbus_login_manager_call_suspend_finish (proxy, res, &err)) + g_warning ("Failed to suspend: %s", err->message); +} + + +static void +on_suspend_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshSuspendManager *self = PHOSH_SUSPEND_MANAGER (data); + + g_return_if_fail (PHOSH_IS_SUSPEND_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_LOGIN_MANAGER_PROXY (self->logind_manager_proxy)); + + phosh_dbus_login_manager_call_suspend (self->logind_manager_proxy, + TRUE, + self->cancel, + (GAsyncReadyCallback)on_suspend_finished, + self); +} + + + +static GActionEntry entries[] = { + { .name = "suspend.trigger-suspend", .activate = on_suspend_activated }, +}; + + +static void +phosh_suspend_manager_init (PhoshSuspendManager *self) +{ + self->cancel = g_cancellable_new (); + self->inhibitors = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + uninhibit); + g_action_map_add_action_entries (G_ACTION_MAP (phosh_shell_get_default ()), + entries, + G_N_ELEMENTS (entries), + self); +} + + +PhoshSuspendManager * +phosh_suspend_manager_new (void) +{ + return PHOSH_SUSPEND_MANAGER (g_object_new (PHOSH_TYPE_SUSPEND_MANAGER, NULL)); +} diff --git a/src/suspend-manager.h b/src/suspend-manager.h new file mode 100644 index 000000000..115ac8367 --- /dev/null +++ b/src/suspend-manager.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "manager.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SUSPEND_MANAGER (phosh_suspend_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshSuspendManager, phosh_suspend_manager, PHOSH, SUSPEND_MANAGER, PhoshManager) + +PhoshSuspendManager *phosh_suspend_manager_new (void); + +G_END_DECLS diff --git a/src/swipe-away-bin.c b/src/swipe-away-bin.c new file mode 100644 index 000000000..bf9d3c464 --- /dev/null +++ b/src/swipe-away-bin.c @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ + +#include "phosh-config.h" + +#include "animation.h" +#include "swipe-away-bin.h" +#include + +enum { + REMOVED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + + +enum { + PROP_0, + PROP_ENABLED, + PROP_ALLOW_NEGATIVE, + PROP_RESERVE_SIZE, + PROP_ORIENTATION, + + LAST_PROP = PROP_ORIENTATION +}; +static GParamSpec *props[LAST_PROP]; + + +struct _PhoshSwipeAwayBin { + GtkEventBox parent_instance; + + GtkOrientation orientation; + gboolean allow_negative; + gboolean reserve_size; + + double progress; + int distance; + HdySwipeTracker *tracker; + PhoshAnimation *animation; + + gboolean enabled; +}; + +static void phosh_swipe_away_bin_swipeable_init (HdySwipeableInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSwipeAwayBin, phosh_swipe_away_bin, GTK_TYPE_EVENT_BOX, + G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL) + G_IMPLEMENT_INTERFACE (HDY_TYPE_SWIPEABLE, phosh_swipe_away_bin_swipeable_init)) + + +static void +set_progress (PhoshSwipeAwayBin *self, + double progress) +{ + self->progress = progress; + + gtk_widget_set_opacity (GTK_WIDGET (self), hdy_ease_out_cubic (1 - ABS (self->progress))); + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + + +static void +animation_value_cb (double value, + PhoshSwipeAwayBin *self) +{ + set_progress (self, value); +} + + +static gboolean +animation_done_idle_cb (PhoshSwipeAwayBin *self) +{ + g_signal_emit (self, signals[REMOVED], 0); + + return G_SOURCE_REMOVE; +} + + +static void +animation_done_cb (PhoshSwipeAwayBin *self) +{ + guint id; + + g_clear_pointer (&self->animation, phosh_animation_unref); + + if (ABS (self->progress) < 1) + return; + + id = g_idle_add ((GSourceFunc) animation_done_idle_cb, self); + g_source_set_name_by_id (id, "[SwipeAwayBin] idle"); +} + + +static void +animate (PhoshSwipeAwayBin *self, + gint64 duration, + double to, + PhoshAnimationType type) +{ + self->animation = + phosh_animation_new (GTK_WIDGET (self), + self->progress, + to, + duration, + type, + (PhoshAnimationValueCallback) animation_value_cb, + (PhoshAnimationDoneCallback) animation_done_cb, + self); + + phosh_animation_start (self->animation); +} + + +static void +begin_swipe_cb (PhoshSwipeAwayBin *self) +{ + if (!self->enabled) + return; + + if (self->animation) + phosh_animation_stop (self->animation); +} + + +static void +update_swipe_cb (PhoshSwipeAwayBin *self, + double progress) +{ + if (!self->enabled) + return; + + set_progress (self, progress); +} + + +static void +end_swipe_cb (PhoshSwipeAwayBin *self, + gint64 duration, + double to) +{ + if (!self->enabled) + return; + + animate (self, duration, to, PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC); +} + + +static void +update_orientation (PhoshSwipeAwayBin *self) +{ + gboolean reversed = + self->orientation == GTK_ORIENTATION_HORIZONTAL && + gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL; + + hdy_swipe_tracker_set_reversed (self->tracker, reversed); + + gtk_widget_queue_allocate (GTK_WIDGET (self)); +} + + +static void +phosh_swipe_away_bin_finalize (GObject *object) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (object); + + g_object_unref (self->tracker); + g_clear_pointer (&self->animation, phosh_animation_unref); + + G_OBJECT_CLASS (phosh_swipe_away_bin_parent_class)->finalize (object); +} + + +static void +phosh_swipe_away_bin_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (object); + + switch (prop_id) { + case PROP_ENABLED: + g_value_set_boolean (value, phosh_swipe_away_bin_get_enabled (self)); + break; + + case PROP_ALLOW_NEGATIVE: + g_value_set_boolean (value, phosh_swipe_away_bin_get_allow_negative (self)); + break; + + case PROP_RESERVE_SIZE: + g_value_set_boolean (value, phosh_swipe_away_bin_get_reserve_size (self)); + break; + + case PROP_ORIENTATION: + g_value_set_enum (value, self->orientation); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +phosh_swipe_away_bin_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (object); + + switch (prop_id) { + case PROP_ENABLED: + phosh_swipe_away_bin_set_enabled (self, g_value_get_boolean (value)); + break; + + case PROP_ALLOW_NEGATIVE: + phosh_swipe_away_bin_set_allow_negative (self, g_value_get_boolean (value)); + break; + + case PROP_RESERVE_SIZE: + phosh_swipe_away_bin_set_reserve_size (self, g_value_get_boolean (value)); + break; + + case PROP_ORIENTATION: + { + GtkOrientation orientation = g_value_get_enum (value); + if (orientation != self->orientation) { + self->orientation = orientation; + update_orientation (self); + g_object_notify (G_OBJECT (self), "orientation"); + } + } + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +phosh_swipe_away_bin_size_allocate (GtkWidget *widget, + GtkAllocation *alloc) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (widget); + GtkWidget *child = gtk_bin_get_child (GTK_BIN (widget)); + GtkAllocation child_alloc; + + GTK_WIDGET_CLASS (phosh_swipe_away_bin_parent_class)->size_allocate (widget, alloc); + + if (!child || !gtk_widget_get_visible (child)) + return; + + child_alloc.y = alloc->y; + child_alloc.x = alloc->x; + child_alloc.width = alloc->width; + child_alloc.height = alloc->height; + + if (self->orientation == GTK_ORIENTATION_HORIZONTAL) { + if (self->reserve_size) { + self->distance = alloc->width / 3; + + child_alloc.width = self->distance; + child_alloc.x += self->distance; + } else { + self->distance = alloc->width; + } + + if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) + child_alloc.x += (int) (self->progress * self->distance); + else + child_alloc.x -= (int) (self->progress * self->distance); + + } else { + if (self->reserve_size) { + self->distance = alloc->height / 3; + + child_alloc.height = self->distance; + child_alloc.y += self->distance; + } else { + self->distance = alloc->height; + } + + child_alloc.y -= (int) (self->progress * self->distance); + } + + + gtk_widget_size_allocate (child, &child_alloc); +} + + +static void +phosh_swipe_away_bin_get_preferred_width (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (widget); + + GTK_WIDGET_CLASS (phosh_swipe_away_bin_parent_class)->get_preferred_width (widget, minimum, natural); + + if (self->reserve_size && + self->orientation == GTK_ORIENTATION_HORIZONTAL) { + if (minimum) + *minimum *= 3; + + if (natural) + *natural *= 3; + } +} + + +static void +phosh_swipe_away_bin_get_preferred_height (GtkWidget *widget, + gint *minimum, + gint *natural) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (widget); + + GTK_WIDGET_CLASS (phosh_swipe_away_bin_parent_class)->get_preferred_height (widget, minimum, natural); + + if (self->reserve_size && + self->orientation == GTK_ORIENTATION_VERTICAL) { + if (minimum) + *minimum *= 3; + + if (natural) + *natural *= 3; + } +} + + +static void +phosh_swipe_away_bin_direction_changed (GtkWidget *widget, + GtkTextDirection previous_direction) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (widget); + + update_orientation (self); +} + + +static void +phosh_swipe_away_bin_class_init (PhoshSwipeAwayBinClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->finalize = phosh_swipe_away_bin_finalize; + object_class->get_property = phosh_swipe_away_bin_get_property; + object_class->set_property = phosh_swipe_away_bin_set_property; + widget_class->size_allocate = phosh_swipe_away_bin_size_allocate; + widget_class->get_preferred_width = phosh_swipe_away_bin_get_preferred_width; + widget_class->get_preferred_height = phosh_swipe_away_bin_get_preferred_height; + widget_class->direction_changed = phosh_swipe_away_bin_direction_changed; + + /** + * PhoshSwipeAwayBin:enabled: + * + * Whether the widget reacts to swipes + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + TRUE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_ALLOW_NEGATIVE] = + g_param_spec_boolean ("allow-negative", + "Allow Negative", + "Use [-1:1] progress range instead of [0:1]", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_RESERVE_SIZE] = + g_param_spec_boolean ("reserve-size", + "Reserve Size", + "Allocate larger size than the child so that the child is never clipped when animating", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, LAST_PROP, props); + + g_object_class_override_property (object_class, + PROP_ORIENTATION, + "orientation"); + + signals[REMOVED] = + g_signal_new ("removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, + NULL, + NULL, + G_TYPE_NONE, + 0); +} + + +static void +phosh_swipe_away_bin_init (PhoshSwipeAwayBin *self) +{ + self->enabled = TRUE; + self->tracker = hdy_swipe_tracker_new (HDY_SWIPEABLE (self)); + + g_object_bind_property (self, "orientation", + self->tracker, "orientation", + G_BINDING_SYNC_CREATE); + hdy_swipe_tracker_set_allow_mouse_drag (self->tracker, TRUE); + update_orientation (self); + + g_signal_connect_object (self->tracker, "begin-swipe", + G_CALLBACK (begin_swipe_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->tracker, "update-swipe", + G_CALLBACK (update_swipe_cb), self, + G_CONNECT_SWAPPED); + g_signal_connect_object (self->tracker, "end-swipe", + G_CALLBACK (end_swipe_cb), self, + G_CONNECT_SWAPPED); +} + + +static HdySwipeTracker * +phosh_swipe_away_bin_get_swipe_tracker (HdySwipeable *swipeable) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (swipeable); + + return self->tracker; +} + + +static double +phosh_swipe_away_bin_get_distance (HdySwipeable *swipeable) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (swipeable); + + return self->distance; +} + + +static double +phosh_swipe_away_bin_get_cancel_progress (HdySwipeable *swipeable) +{ + return 0; +} + + +static double +phosh_swipe_away_bin_get_progress (HdySwipeable *swipeable) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (swipeable); + + return self->progress; +} + + +static double * +phosh_swipe_away_bin_get_snap_points (HdySwipeable *swipeable, + int *n_snap_points) +{ + PhoshSwipeAwayBin *self = PHOSH_SWIPE_AWAY_BIN (swipeable); + int n = self->allow_negative ? 3 : 2; + double *points; + + points = g_new0 (double, n); + + if (self->allow_negative) { + points[0] = -1; + points[2] = 1; + } else { + points[1] = 1; + } + + if (n_snap_points) + *n_snap_points = n; + + return points; +} + + +static void +phosh_swipe_away_bin_switch_child (HdySwipeable *swipeable, + guint index, + gint64 duration) +{ +} + + +static void +phosh_swipe_away_bin_swipeable_init (HdySwipeableInterface *iface) +{ + iface->get_swipe_tracker = phosh_swipe_away_bin_get_swipe_tracker; + iface->get_distance = phosh_swipe_away_bin_get_distance; + iface->get_cancel_progress = phosh_swipe_away_bin_get_cancel_progress; + iface->get_progress = phosh_swipe_away_bin_get_progress; + iface->get_snap_points = phosh_swipe_away_bin_get_snap_points; + iface->switch_child = phosh_swipe_away_bin_switch_child; +} + + +gboolean +phosh_swipe_away_bin_get_enabled (PhoshSwipeAwayBin *self) +{ + g_return_val_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self), FALSE); + + return self->enabled; +} + + +void +phosh_swipe_away_bin_set_enabled (PhoshSwipeAwayBin *self, + gboolean enabled) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + enabled = !!enabled; + if (enabled == self->enabled) + return; + + self->enabled = enabled; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + + +gboolean +phosh_swipe_away_bin_get_allow_negative (PhoshSwipeAwayBin *self) +{ + g_return_val_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self), FALSE); + + return self->allow_negative; +} + + +void +phosh_swipe_away_bin_set_allow_negative (PhoshSwipeAwayBin *self, + gboolean allow_negative) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + allow_negative = !!allow_negative; + + if (allow_negative == self->allow_negative) + return; + + self->allow_negative = allow_negative; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_NEGATIVE]); +} + + +gboolean +phosh_swipe_away_bin_get_reserve_size (PhoshSwipeAwayBin *self) +{ + g_return_val_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self), FALSE); + + return self->reserve_size; +} + + +void +phosh_swipe_away_bin_set_reserve_size (PhoshSwipeAwayBin *self, + gboolean reserve_size) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + reserve_size = !!reserve_size; + + if (reserve_size == self->reserve_size) + return; + + self->reserve_size = reserve_size; + + gtk_widget_queue_resize (GTK_WIDGET (self)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESERVE_SIZE]); +} + + +void +phosh_swipe_away_bin_hide (PhoshSwipeAwayBin *self) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + if (self->animation) + phosh_animation_stop (self->animation); + + set_progress (self, 1); +} + + +void +phosh_swipe_away_bin_reveal (PhoshSwipeAwayBin *self) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + if (self->animation) + phosh_animation_stop (self->animation); + + animate (self, 200, 0, PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC); +} + + +void +phosh_swipe_away_bin_remove (PhoshSwipeAwayBin *self) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + if (self->animation) + phosh_animation_stop (self->animation); + + animate (self, 200, 1, PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC); +} + + +void +phosh_swipe_away_bin_undo (PhoshSwipeAwayBin *self) +{ + g_return_if_fail (PHOSH_IS_SWIPE_AWAY_BIN (self)); + + if (self->animation) + phosh_animation_stop (self->animation); + + animate (self, 600, 0, PHOSH_ANIMATION_TYPE_EASE_OUT_BOUNCE); +} diff --git a/src/swipe-away-bin.h b/src/swipe-away-bin.h new file mode 100644 index 000000000..39217d8ab --- /dev/null +++ b/src/swipe-away-bin.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Alexander Mikhaylenko + */ +#pragma once + +#include + +#define PHOSH_TYPE_SWIPE_AWAY_BIN (phosh_swipe_away_bin_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshSwipeAwayBin, phosh_swipe_away_bin, PHOSH, SWIPE_AWAY_BIN, GtkEventBox) + +gboolean phosh_swipe_away_bin_get_enabled (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_set_enabled (PhoshSwipeAwayBin *self, gboolean enabled); + +gboolean phosh_swipe_away_bin_get_allow_negative (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_set_allow_negative (PhoshSwipeAwayBin *self, + gboolean allow_negative); + +gboolean phosh_swipe_away_bin_get_reserve_size (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_set_reserve_size (PhoshSwipeAwayBin *self, + gboolean reserve_size); + +void phosh_swipe_away_bin_hide (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_reveal (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_remove (PhoshSwipeAwayBin *self); +void phosh_swipe_away_bin_undo (PhoshSwipeAwayBin *self); diff --git a/src/system-modal-dialog.c b/src/system-modal-dialog.c new file mode 100644 index 000000000..a795b5a3b --- /dev/null +++ b/src/system-modal-dialog.c @@ -0,0 +1,463 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-system-modal-dialog" + +#include "phosh-config.h" + +#include "animation.h" +#include "layersurface-priv.h" +#include "shell-priv.h" +#include "system-modal-dialog.h" +#include "swipe-away-bin.h" +#include "util.h" + +#include + +/** + * PhoshSystemModalDialog: + * + * A modal system dialog + * + * The #PhoshSystemModalDialog is used as a base class for system modal dialogs + * such as #PhoshSystemPrompt or #PhoshNetworkAuthPrompt. It consists of a title + * at the top, a content widget below that and button are at the bottom. + * The content widget can be set via #phosh_system_modal_dialog_set_content() and buttons + * to the button area added via #phosh_system_modal_dialog_add_button(). + * + * # CSS Style classes + * + * A system modal dialog uses several style classes for consistent layout: + * ".phosh-system-modal-dialog" for the whole dialog area, + * ".phosh-system-modal-dialog-title" for the dialog title, + * ".phosh-system-modal-dialog-content" for the content area and + * ".phosh-system-modal-dialog-buttons" for the button area. + * + * # PhoshSystemModalDialog as #GtkBuildable + * + * The content widget and buttons can be specified using type + * <phosh-dialog-content> and <phosh-dialog-button> type attributes: + * + * |[ + * + * + * + * True + * vertical + * + * ... + * + * + * + * + * + * Ok + * ... + * + * + * + * + * Cancel + * ... + * + * + * + * ]| + */ + +enum { + PROP_0, + PROP_TITLE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + DIALOG_CANCELED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct { + char *title; + + GtkWidget *lbl_title; + GtkWidget *box_dialog; + GtkWidget *box_buttons; + + gboolean fade_out; + PhoshAnimation *animation; +} PhoshSystemModalDialogPrivate; + +static void phosh_system_modal_dialog_buildable_init (GtkBuildableIface *iface); + +G_DEFINE_TYPE_WITH_CODE (PhoshSystemModalDialog, phosh_system_modal_dialog, + PHOSH_TYPE_SYSTEM_MODAL, + G_ADD_PRIVATE (PhoshSystemModalDialog) + G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, + phosh_system_modal_dialog_buildable_init)) + +static void +phosh_system_modal_dialog_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSystemModalDialog *self = PHOSH_SYSTEM_MODAL_DIALOG (obj); + + switch (prop_id) { + case PROP_TITLE: + phosh_system_modal_dialog_set_title (self, g_value_get_string (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_system_modal_dialog_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSystemModalDialog *self = PHOSH_SYSTEM_MODAL_DIALOG (obj); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, phosh_system_modal_dialog_get_title (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_system_modal_dialog_map (GtkWidget *widget) +{ + PhoshSystemModalDialog *self = PHOSH_SYSTEM_MODAL_DIALOG (widget); + PhoshSystemModalDialogPrivate *priv = phosh_system_modal_dialog_get_instance_private (self); + + GTK_WIDGET_CLASS (phosh_system_modal_dialog_parent_class)->map (widget); + + phosh_animation_start (priv->animation); +} + + +static void +on_removed_by_swipe (PhoshSystemModalDialog *self) +{ + g_signal_emit (self, signals[DIALOG_CANCELED], 0); +} + + +static void +phosh_system_modal_dialog_finalize (GObject *object) +{ + PhoshSystemModalDialog *self = PHOSH_SYSTEM_MODAL_DIALOG (object); + PhoshSystemModalDialogPrivate *priv = phosh_system_modal_dialog_get_instance_private (self); + + g_clear_pointer (&priv->animation, phosh_animation_unref); + g_clear_pointer (&priv->title, g_free); + + G_OBJECT_CLASS (phosh_system_modal_dialog_parent_class)->finalize (object); +} + + +static void +phosh_system_modal_dialog_class_init (PhoshSystemModalDialogClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkBindingSet *binding_set; + + object_class->get_property = phosh_system_modal_dialog_get_property; + object_class->set_property = phosh_system_modal_dialog_set_property; + object_class->finalize = phosh_system_modal_dialog_finalize; + + widget_class->map = phosh_system_modal_dialog_map; + + /** + * PhoshSystemModalDialog:title + * + * The dialog's title + */ + props[PROP_TITLE] = g_param_spec_string ("title", "", "", + NULL, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshSystemModalDialog::dialog-canceled: + * + * The ::dialog-canceled signal is emitted when the dialog was canceled and should be + * hidden or destroyed. + */ + signals[DIALOG_CANCELED] = g_signal_new ("dialog-canceled", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + g_type_ensure (PHOSH_TYPE_SWIPE_AWAY_BIN); + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/system-modal-dialog.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemModalDialog, lbl_title); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemModalDialog, box_dialog); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemModalDialog, box_buttons); + gtk_widget_class_bind_template_callback (widget_class, on_removed_by_swipe); + + binding_set = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "dialog-canceled", 0); +} + + +static GtkBuildableIface *parent_buildable_iface; + + +static void +phosh_system_modal_dialog_buildable_add_child (GtkBuildable *buildable, + GtkBuilder *builder, + GObject *child, + const char *type) +{ + PhoshSystemModalDialog *self = PHOSH_SYSTEM_MODAL_DIALOG (buildable); + + if (g_strcmp0 (type, "phosh-dialog-content") == 0) { + phosh_system_modal_dialog_set_content (self, GTK_WIDGET (child)); + return; + } + + if (g_strcmp0 (type, "phosh-dialog-button") == 0) { + phosh_system_modal_dialog_add_button (self, GTK_WIDGET (child), -1); + return; + } + + /* The parent is a container itself so chain up */ + parent_buildable_iface->add_child (buildable, builder, child, type); +} + + +static void +phosh_system_modal_dialog_buildable_init (GtkBuildableIface *iface) +{ + parent_buildable_iface = g_type_interface_peek_parent (iface); + iface->add_child = phosh_system_modal_dialog_buildable_add_child; +} + + +static void +animation_value_cb (double value, PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv = phosh_system_modal_dialog_get_instance_private (self); + + if (priv->fade_out) + value = 1.0 - value; + + phosh_layer_surface_set_alpha (PHOSH_LAYER_SURFACE (self), value); +} + + +static void +animation_done_cb (PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv = phosh_system_modal_dialog_get_instance_private (self); + + g_clear_pointer (&priv->animation, phosh_animation_unref); + + if (priv->fade_out) + gtk_widget_destroy (GTK_WIDGET (self)); +} + + +static void +phosh_system_modal_dialog_init (PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv = phosh_system_modal_dialog_get_instance_private (self); + + gtk_widget_init_template (GTK_WIDGET (self)); + + priv->animation = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + 150 * PHOSH_ANIMATION_SLOWDOWN, + PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC, + (PhoshAnimationValueCallback) animation_value_cb, + (PhoshAnimationDoneCallback) animation_done_cb, + self); +} + +/** + * phosh_system_modal_dialog_new: + * + * Create a new system-modal dialog. + * + * Returns: A new system modal dialog + */ +GtkWidget * +phosh_system_modal_dialog_new (void) +{ + return g_object_new (PHOSH_TYPE_SYSTEM_MODAL_DIALOG, NULL); +} + + +/** + * phosh_system_modal_dialog_set_content: + * @self: The #PhoshSystemModalDialog + * @content: The widget for the dialog's content area + * + * Adds the given widget as the dialog's content area. It is a programming error + * to set the content more than once. + */ +void +phosh_system_modal_dialog_set_content (PhoshSystemModalDialog *self, GtkWidget *content) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self)); + g_return_if_fail (GTK_IS_WIDGET (content)); + + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + gtk_box_pack_start (GTK_BOX (priv->box_dialog), GTK_WIDGET (content), FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (priv->box_dialog), GTK_WIDGET (content), 1); + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (content)), + "phosh-system-modal-dialog-content"); +} + + +/** + * phosh_system_modal_dialog_add_button: + * @self: The #PhoshSystemModalDialog + * @button: The button for the dialog's button area + * @position: The buttons position in the box or -1 + * + * Adds the given button to the dialog's content area at the given position. If + * the posiion is `-1` the button is appended at the end. + */ +void +phosh_system_modal_dialog_add_button (PhoshSystemModalDialog *self, GtkWidget *button, gint position) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self)); + g_return_if_fail (GTK_IS_BUTTON (button)); + + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + gtk_box_pack_start (GTK_BOX (priv->box_buttons), GTK_WIDGET (button), TRUE, TRUE, 0); + if (position >= 0) + gtk_box_reorder_child (GTK_BOX (priv->box_buttons), GTK_WIDGET (button), position); +} + + +void +phosh_system_modal_dialog_remove_button (PhoshSystemModalDialog *self, GtkWidget *button) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self)); + g_return_if_fail (GTK_IS_BUTTON (button)); + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + gtk_container_remove (GTK_CONTAINER (priv->box_buttons), button); +} + +/** + * phosh_system_modal_dialog_get_buttons: + * @self: A modal dialog + * + * Get the dialog's buttons + * + * Returns:(element-type GtkWidget)(transfer container): The buttons + */ +GList * +phosh_system_modal_dialog_get_buttons (PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self), NULL); + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + return gtk_container_get_children (GTK_CONTAINER (priv->box_buttons)); +} + +/** + * phosh_system_modal_dialog_get_title: + * @self: The dialog + * + * Get the dialog's title. + * + * Returns: The dialog's title + * + * Since: 0.44.0 + */ +const char * +phosh_system_modal_dialog_get_title (PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_val_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self), NULL); + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + return priv->title; +} + +void +phosh_system_modal_dialog_set_title (PhoshSystemModalDialog *self, const char *title) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self)); + priv = phosh_system_modal_dialog_get_instance_private (PHOSH_SYSTEM_MODAL_DIALOG (self)); + + if (g_strcmp0 (priv->title, title) == 0) + return; + + g_free (priv->title); + priv->title = g_strdup (title); + + gtk_label_set_label (GTK_LABEL (priv->lbl_title), priv->title); + gtk_widget_set_visible (priv->lbl_title, !gm_str_is_null_or_empty (priv->title)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); +} + +/** + * phosh_system_modal_dialog_close: + * @self: The dialog to close + * + * Hides the dialog and destroys it. When the compositor supports it + * uses an animation. If you want to destroy the dialog directly use + * `gtk_widget_destroy()`. + */ +void +phosh_system_modal_dialog_close (PhoshSystemModalDialog *self) +{ + PhoshSystemModalDialogPrivate *priv; + + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL_DIALOG (self)); + priv = phosh_system_modal_dialog_get_instance_private (self); + + priv->fade_out = TRUE; + priv->animation = phosh_animation_new (GTK_WIDGET (self), + 0.0, + 1.0, + 150 * PHOSH_ANIMATION_SLOWDOWN, + PHOSH_ANIMATION_TYPE_EASE_OUT_CUBIC, + (PhoshAnimationValueCallback) animation_value_cb, + (PhoshAnimationDoneCallback) animation_done_cb, + self); + phosh_animation_start (priv->animation); +} diff --git a/src/system-modal-dialog.h b/src/system-modal-dialog.h new file mode 100644 index 000000000..2a4faaadc --- /dev/null +++ b/src/system-modal-dialog.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021-2023 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "system-modal.h" + +#include + +#define PHOSH_TYPE_SYSTEM_MODAL_DIALOG (phosh_system_modal_dialog_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshSystemModalDialog, phosh_system_modal_dialog, PHOSH, SYSTEM_MODAL_DIALOG, PhoshSystemModal) + +/** + * PhoshSystemModalDialogClass + * @parent_class: The parent class + */ +struct _PhoshSystemModalDialogClass { + PhoshSystemModalClass parent_class; +}; + + +GtkWidget *phosh_system_modal_dialog_new (void); +void phosh_system_modal_dialog_set_content (PhoshSystemModalDialog *self, GtkWidget *content); +void phosh_system_modal_dialog_add_button (PhoshSystemModalDialog *self, GtkWidget *button, gint position); +void phosh_system_modal_dialog_set_title (PhoshSystemModalDialog *self, const char *title); +const char *phosh_system_modal_dialog_get_title (PhoshSystemModalDialog *self); +void phosh_system_modal_dialog_remove_button (PhoshSystemModalDialog *self, GtkWidget *button); +GList *phosh_system_modal_dialog_get_buttons (PhoshSystemModalDialog *self); +void phosh_system_modal_dialog_close (PhoshSystemModalDialog *self); diff --git a/src/system-modal.c b/src/system-modal.c new file mode 100644 index 000000000..f34606107 --- /dev/null +++ b/src/system-modal.c @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-system-modal" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "system-modal.h" + +/** + * PhoshSystemModal: + * + * A modal system component + * + * The #PhoshSystemModal is used as a base class for other + * system components such as dialogs like #PhoshSystemPrompt or + * the OSD display. + */ + +enum { + PROP_0, + PROP_MONITOR, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct { + PhoshMonitor *monitor; +} PhoshSystemModalPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshSystemModal, phosh_system_modal, PHOSH_TYPE_LAYER_SURFACE); + +/* + * Keep track of opened modals + * Only reset PHOSH_STATE_MODAL_SYSTEM_PROMPT when there are no more modals + * see phosh_system_modal_map/unmap + */ +static int modal_count = 0; + + +static void +phosh_system_modal_map (GtkWidget *widget) +{ + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL (widget)); + + modal_count++; + phosh_shell_set_state (phosh_shell_get_default (), PHOSH_STATE_MODAL_SYSTEM_PROMPT, TRUE); + + GTK_WIDGET_CLASS (phosh_system_modal_parent_class)->map (widget); +} + + +static void +phosh_system_modal_unmap (GtkWidget *widget) +{ + g_return_if_fail (PHOSH_IS_SYSTEM_MODAL (widget)); + + modal_count--; + + if (modal_count == 0) + phosh_shell_set_state (phosh_shell_get_default (), PHOSH_STATE_MODAL_SYSTEM_PROMPT, FALSE); + else if (modal_count < 0) + g_warning ("The modal counter is negative %d. This should never happen", + modal_count); + + GTK_WIDGET_CLASS (phosh_system_modal_parent_class)->unmap (widget); +} + +static void +phosh_system_modal_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSystemModal *self = PHOSH_SYSTEM_MODAL (obj); + PhoshSystemModalPrivate *priv = phosh_system_modal_get_instance_private (self); + + switch (prop_id) { + case PROP_MONITOR: + g_set_object (&priv->monitor, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_system_modal_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSystemModal *self = PHOSH_SYSTEM_MODAL (obj); + PhoshSystemModalPrivate *priv = phosh_system_modal_get_instance_private (self); + + switch (prop_id) { + case PROP_MONITOR: + g_value_set_object (value, priv->monitor); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_system_modal_dispose (GObject *obj) +{ + PhoshSystemModal *self = PHOSH_SYSTEM_MODAL (obj); + PhoshSystemModalPrivate *priv = phosh_system_modal_get_instance_private (self); + + g_clear_object (&priv->monitor); + + G_OBJECT_CLASS (phosh_system_modal_parent_class)->dispose (obj); +} + + +static void +phosh_system_modal_constructed (GObject *object) +{ + PhoshSystemModal *self = PHOSH_SYSTEM_MODAL (object); + PhoshSystemModalPrivate *priv = phosh_system_modal_get_instance_private (self); + PhoshWayland *wl = phosh_wayland_get_default (); + + if (priv->monitor == NULL) + priv->monitor = g_object_ref (phosh_shell_get_primary_monitor (phosh_shell_get_default ())); + + g_object_set (PHOSH_LAYER_SURFACE (self), + "layer-shell", phosh_wayland_get_zwlr_layer_shell_v1 (wl), + "wl-output", phosh_monitor_get_wl_output (priv->monitor), + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, + "layer", ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY, + "kbd-interactivity", TRUE, + "exclusive-zone", -1, + "namespace", "phosh system-modal", + NULL); + + gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)), + "phosh-system-modal"); + + G_OBJECT_CLASS (phosh_system_modal_parent_class)->constructed (object); +} + + +static void +phosh_system_modal_class_init (PhoshSystemModalClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + widget_class->map = phosh_system_modal_map; + widget_class->unmap = phosh_system_modal_unmap; + + object_class->get_property = phosh_system_modal_get_property; + object_class->set_property = phosh_system_modal_set_property; + object_class->constructed = phosh_system_modal_constructed; + object_class->dispose = phosh_system_modal_dispose; + + /** + * PhoshSystemModal:monitor: + * + * The monitor this system modal should appear on + */ + props[PROP_MONITOR] = + g_param_spec_object ("monitor", "", "", + PHOSH_TYPE_MONITOR, + G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_system_modal_init (PhoshSystemModal *self) +{ +} + +/** + * phosh_system_modal_new: + * @monitor: The #PhoshMonitor to put the modal surface on. If %NULL the primary monitor is used. + * + * Create a new system-modal surface. + */ +GtkWidget * +phosh_system_modal_new (PhoshMonitor *monitor) +{ + g_return_val_if_fail (PHOSH_IS_MONITOR (monitor) || monitor == NULL, NULL); + + return g_object_new (PHOSH_TYPE_SYSTEM_MODAL, "monitor", monitor, NULL); +} diff --git a/src/system-modal.h b/src/system-modal.h new file mode 100644 index 000000000..e565cdf9b --- /dev/null +++ b/src/system-modal.h @@ -0,0 +1,26 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "layersurface.h" +#include "monitor/monitor.h" + +#define PHOSH_TYPE_SYSTEM_MODAL (phosh_system_modal_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshSystemModal, phosh_system_modal, PHOSH, SYSTEM_MODAL, PhoshLayerSurface) + +/** + * PhoshSystemModalClass + * @parent_class: The parent class + */ +struct _PhoshSystemModalClass { + PhoshLayerSurfaceClass parent_class; +}; + + +GtkWidget *phosh_system_modal_new (PhoshMonitor *monitor); diff --git a/src/system-prompt.c b/src/system-prompt.c new file mode 100644 index 000000000..6659a0004 --- /dev/null +++ b/src/system-prompt.c @@ -0,0 +1,718 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Based on gnome-shell's shell-keyring-prompt.c + * Author: Stef Walter + */ + +#define G_LOG_DOMAIN "phosh-system-prompt" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "system-prompt.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include + +#include + +/** + * PhoshSystemPrompt: + * + * A modal system prompt + * + * The #PhoshSystemPrompt is used to ask for PINs and passwords + */ + +enum { + PROP_0, + + /* GcrPromptIface */ + PROP_MESSAGE, + PROP_DESCRIPTION, + PROP_WARNING, + PROP_CHOICE_LABEL, + PROP_CHOICE_CHOSEN, + PROP_PASSWORD_NEW, + PROP_PASSWORD_STRENGTH, + PROP_CALLER_WINDOW, + PROP_CONTINUE_LABEL, + PROP_CANCEL_LABEL, + + /* our own */ + PROP_PASSWORD_VISIBLE, + PROP_CONFIRM_VISIBLE, + PROP_WARNING_VISIBLE, + PROP_CHOICE_VISIBLE, + PROP_LAST_PROP, +}; + +typedef enum +{ + PROMPTING_NONE, + PROMPTING_FOR_CONFIRM, + PROMPTING_FOR_PASSWORD +} PromptingMode; + + +typedef struct +{ + char *message; + char *description; + char *warning; + char *choice_label; + gboolean choice_chosen; + gboolean password_new; + guint password_strength; + char *continue_label; + char *cancel_label; + + GtkWidget *btn_cancel; + GtkWidget *btn_continue; + GtkWidget *checkbtn_choice; + GtkWidget *entry_confirm; + GtkWidget *entry_password; + GtkWidget *grid; + GtkWidget *lbl_choice; + GtkWidget *lbl_confirm; + GtkWidget *lbl_description; + GtkWidget *lbl_password; + GtkWidget *lbl_warning; + GtkWidget *pbar_quality; + + GtkEntryBuffer *password_buffer; + GtkEntryBuffer *confirm_buffer; + + GTask *task; + GcrPromptReply last_reply; + PromptingMode mode; + gboolean shown; +} PhoshSystemPromptPrivate; + + +struct _PhoshSystemPrompt +{ + PhoshSystemModalDialog parent; +}; + + +static void phosh_system_prompt_iface_init (GcrPromptIface *iface); +G_DEFINE_TYPE_WITH_CODE(PhoshSystemPrompt, phosh_system_prompt, PHOSH_TYPE_SYSTEM_MODAL_DIALOG, + G_IMPLEMENT_INTERFACE (GCR_TYPE_PROMPT, + phosh_system_prompt_iface_init) + G_ADD_PRIVATE (PhoshSystemPrompt)); + + +static void +phosh_system_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (obj); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + switch (prop_id) { + case PROP_MESSAGE: + g_free (priv->message); + priv->message = g_value_dup_string (value); + g_object_notify (obj, "message"); + break; + case PROP_DESCRIPTION: + g_free (priv->description); + priv->description = g_value_dup_string (value); + g_object_notify (obj, "description"); + break; + case PROP_WARNING: + g_free (priv->warning); + priv->warning = g_value_dup_string (value); + g_object_notify (obj, "warning"); + g_object_notify (obj, "warning-visible"); + break; + case PROP_CHOICE_LABEL: + g_free (priv->choice_label); + priv->choice_label = g_value_dup_string (value); + g_object_notify (obj, "choice-label"); + g_object_notify (obj, "choice-visible"); + break; + case PROP_CHOICE_CHOSEN: + priv->choice_chosen = g_value_get_boolean (value); + g_object_notify (obj, "choice-chosen"); + break; + case PROP_PASSWORD_NEW: + priv->password_new = g_value_get_boolean (value); + g_object_notify (obj, "password-new"); + g_object_notify (obj, "confirm-visible"); + break; + case PROP_CALLER_WINDOW: + /* ignored */ + break; + case PROP_CONTINUE_LABEL: + g_free (priv->continue_label); + priv->continue_label = g_value_dup_string (value); + g_object_notify (obj, "continue-label"); + break; + case PROP_CANCEL_LABEL: + g_free (priv->cancel_label); + priv->cancel_label = g_value_dup_string (value); + g_object_notify (obj, "cancel-label"); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + +static void +phosh_system_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (obj); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + switch (prop_id) { + case PROP_MESSAGE: + g_value_set_string (value, priv->message ? priv->message : ""); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, priv->description ? priv->description : ""); + break; + case PROP_WARNING: + g_value_set_string (value, priv->warning ? priv->warning : ""); + break; + case PROP_CHOICE_LABEL: + g_value_set_string (value, priv->choice_label ? priv->choice_label : ""); + break; + case PROP_CHOICE_CHOSEN: + g_value_set_boolean (value, priv->choice_chosen); + break; + case PROP_PASSWORD_NEW: + g_value_set_boolean (value, priv->password_new); + break; + case PROP_PASSWORD_STRENGTH: + g_value_set_int (value, priv->password_strength); + break; + case PROP_CALLER_WINDOW: + g_value_set_string (value, ""); + break; + case PROP_CONTINUE_LABEL: + g_value_set_string (value, priv->continue_label); + break; + case PROP_CANCEL_LABEL: + g_value_set_string (value, priv->cancel_label); + break; + case PROP_PASSWORD_VISIBLE: + g_value_set_boolean (value, priv->mode == PROMPTING_FOR_PASSWORD); + break; + case PROP_CONFIRM_VISIBLE: + g_value_set_boolean (value, priv->password_new && + priv->mode == PROMPTING_FOR_CONFIRM); + break; + case PROP_WARNING_VISIBLE: + g_value_set_boolean (value, priv->warning && priv->warning[0]); + break; + case PROP_CHOICE_VISIBLE: + g_value_set_boolean (value, priv->choice_label && priv->choice_label[0]); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + + + +static void +phosh_system_prompt_password_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (prompt); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + GObject *obj; + + g_debug ("Starting system password prompt: %s", __func__); + if (priv->task != NULL) { + g_warning ("this prompt can only show one prompt at a time"); + return; + } + priv->mode = PROMPTING_FOR_PASSWORD; + priv->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (priv->task, phosh_system_prompt_password_async); + + if (!gtk_entry_get_text_length (GTK_ENTRY (priv->entry_password))) + gtk_widget_set_sensitive (priv->btn_continue, FALSE); + gtk_widget_set_sensitive (priv->grid, TRUE); + gtk_widget_grab_focus (priv->entry_password); + + obj = G_OBJECT (self); + g_object_notify (obj, "password-visible"); + g_object_notify (obj, "confirm-visible"); + g_object_notify (obj, "warning-visible"); + g_object_notify (obj, "choice-visible"); + priv->shown = TRUE; + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + self, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); +} + + +static const char * +phosh_system_prompt_password_finish (GcrPrompt *prompt, + GAsyncResult *result, + GError **error) +{ + g_return_val_if_fail (g_task_get_source_object (G_TASK (result)) == prompt, NULL); + g_return_val_if_fail (g_async_result_is_tagged (result, + phosh_system_prompt_password_async), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + + +static void +phosh_system_prompt_close (GcrPrompt *prompt) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (prompt); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + if (!priv->shown) + return; + + priv->shown = FALSE; + phosh_system_modal_dialog_close (PHOSH_SYSTEM_MODAL_DIALOG (self)); +} + + +static void +phosh_system_prompt_confirm_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (prompt); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + GObject *obj; + + g_debug ("Starting system confirmation prompt: %s", __func__); + if (priv->task != NULL) { + g_warning ("this prompt can only show one prompt at a time"); + return; + } + priv->mode = PROMPTING_FOR_CONFIRM; + priv->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (priv->task, phosh_system_prompt_confirm_async); + + gtk_widget_set_sensitive (priv->btn_continue, TRUE); + gtk_widget_set_sensitive (priv->grid, TRUE); + gtk_widget_grab_focus (priv->entry_confirm); + + obj = G_OBJECT (self); + g_object_notify (obj, "password-visible"); + g_object_notify (obj, "confirm-visible"); + g_object_notify (obj, "warning-visible"); + g_object_notify (obj, "choice-visible"); + priv->shown = TRUE; + + /* Show widget when not locked and keep that in sync */ + g_object_bind_property (phosh_shell_get_default (), "locked", + self, "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); +} + + +static GcrPromptReply +phosh_system_prompt_confirm_finish (GcrPrompt *prompt, + GAsyncResult *result, + GError **error) +{ + GTask *task = G_TASK (result); + gssize res; + + g_debug ("Finishing system confirmation prompt: %s", __func__); + g_return_val_if_fail (g_task_get_source_object (task) == prompt, + GCR_PROMPT_REPLY_CANCEL); + g_return_val_if_fail (g_async_result_is_tagged (result, + phosh_system_prompt_confirm_async), GCR_PROMPT_REPLY_CANCEL); + + res = g_task_propagate_int (task, error); + return res == -1 ? GCR_PROMPT_REPLY_CANCEL : (GcrPromptReply)res; +} + + +static gboolean +prompt_complete (PhoshSystemPrompt *self) +{ + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + GTask *res; + PromptingMode mode; + const char *password; + const char *confirm; + const char *env; + + g_return_val_if_fail (PHOSH_IS_SYSTEM_PROMPT (self), FALSE); + g_return_val_if_fail (priv->mode != PROMPTING_NONE, FALSE); + g_return_val_if_fail (priv->task != NULL, FALSE); + + password = gtk_entry_buffer_get_text (priv->password_buffer); + + if (priv->mode == PROMPTING_FOR_CONFIRM) { + /* Is it a new password? */ + if (priv->password_new) { + confirm = gtk_entry_buffer_get_text (priv->confirm_buffer); + /* Do the passwords match? */ + if (!g_str_equal (password, confirm)) { + gcr_prompt_set_warning (GCR_PROMPT (self), _("Passwords do not match.")); + return FALSE; + } + + /* Don't allow blank passwords if in paranoid mode */ + env = g_getenv ("GNOME_KEYRING_PARANOID"); + if (env && *env) { + gcr_prompt_set_warning (GCR_PROMPT (self), _("Password cannot be blank")); + return FALSE; + } + } + } + + res = priv->task; + mode = priv->mode; + priv->task = NULL; + priv->mode = PROMPTING_NONE; + + if (mode == PROMPTING_FOR_CONFIRM) + g_task_return_int (res, (gssize)GCR_PROMPT_REPLY_CONTINUE); + else + g_task_return_pointer (res, (gpointer)password, NULL); + g_object_unref (res); + + gtk_widget_set_sensitive (priv->btn_continue, FALSE); + gtk_widget_set_sensitive (priv->grid, FALSE); + + return TRUE; +} + + +static void +prompt_cancel (PhoshSystemPrompt *self) +{ + PhoshSystemPromptPrivate *priv; + GTask *res; + PromptingMode mode; + + g_return_if_fail (PHOSH_IS_SYSTEM_PROMPT (self)); + + priv = phosh_system_prompt_get_instance_private (self); + g_debug ("Canceling system password prompt for task %p", priv->task); + + /* + * If canceled while not prompting, we should just close the prompt, + * the user wants it to go away. + */ + if (priv->mode == PROMPTING_NONE) { + if (priv->shown) + gcr_prompt_close (GCR_PROMPT (self)); + return; + } + + g_return_if_fail (priv->task != NULL); + + res = priv->task; + mode = priv->mode; + priv->task = NULL; + priv->mode = PROMPTING_NONE; + + if (mode == PROMPTING_FOR_CONFIRM) + g_task_return_int (res, (gssize) GCR_PROMPT_REPLY_CANCEL); + else + g_task_return_pointer (res, NULL, NULL); + g_object_unref (res); +} + + +static void +on_password_changed (PhoshSystemPrompt *self, + GtkEditable *editable) +{ + PhoshSystemPromptPrivate *priv; + int upper, digit, misc; + const char *password; + double pwstrength; + int length, i; + + g_return_if_fail (PHOSH_IS_SYSTEM_PROMPT (self)); + g_return_if_fail (GTK_IS_EDITABLE (editable)); + + priv = phosh_system_prompt_get_instance_private (self); + + if (!gtk_entry_get_text_length (GTK_ENTRY (editable))) + return; + + gtk_widget_set_sensitive (priv->btn_continue, TRUE); + password = gtk_entry_get_text (GTK_ENTRY (editable)); + + /* + * This code is based on the Master Password dialog in Firefox + * (pref-masterpass.js) + * Original code triple-licensed under the MPL, GPL, and LGPL + * so is license-compatible with this file + */ + + length = strlen (password); + upper = 0; + digit = 0; + misc = 0; + + for ( i = 0; i < length ; i++) { + if (g_ascii_isdigit (password[i])) + digit++; + else if (g_ascii_isupper (password[i])) + upper++; + else if (!g_ascii_islower (password[i])) + misc++; + } + + if (length > 5) + length = 5; + if (digit > 3) + digit = 3; + if (upper > 3) + upper = 3; + if (misc > 3) + misc = 3; + + pwstrength = ((length * 0.1) - 0.2) + + (digit * 0.1) + + (misc * 0.15) + + (upper * 0.1); + + if (pwstrength < 0.0) + pwstrength = 0.0; + if (pwstrength > 1.0) + pwstrength = 1.0; + + gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (priv->pbar_quality), pwstrength); +} + + +static void +on_btn_continue_clicked (PhoshSystemPrompt *self, GtkButton *btn) +{ + prompt_complete (self); +} + + +static void +on_dialog_canceled (PhoshSystemPrompt *self) +{ + prompt_cancel (self); +} + +static void +phosh_system_prompt_iface_init (GcrPromptIface *iface) +{ + iface->prompt_confirm_async = phosh_system_prompt_confirm_async; + iface->prompt_confirm_finish = phosh_system_prompt_confirm_finish; + iface->prompt_password_async = phosh_system_prompt_password_async; + iface->prompt_password_finish = phosh_system_prompt_password_finish; + iface->prompt_close = phosh_system_prompt_close; +} + + +static void +phosh_system_prompt_dispose (GObject *obj) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (obj); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + if (priv->shown) + gcr_prompt_close (GCR_PROMPT (self)); + + if (priv->task) + prompt_cancel (self); + + g_assert (priv->task == NULL); + G_OBJECT_CLASS (phosh_system_prompt_parent_class)->dispose (obj); +} + + +static void +phosh_system_prompt_finalize (GObject *obj) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (obj); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + g_free (priv->message); + g_free (priv->description); + g_free (priv->warning); + g_free (priv->choice_label); + g_free (priv->continue_label); + g_free (priv->cancel_label); + + G_OBJECT_CLASS (phosh_system_prompt_parent_class)->finalize (obj); +} + + +static void +phosh_system_prompt_constructed (GObject *object) +{ + PhoshSystemPrompt *self = PHOSH_SYSTEM_PROMPT (object); + PhoshSystemPromptPrivate *priv = phosh_system_prompt_get_instance_private (self); + + G_OBJECT_CLASS (phosh_system_prompt_parent_class)->constructed (object); + + g_object_bind_property (self, "message", self, "title", G_BINDING_DEFAULT); + g_object_bind_property (self, "description", priv->lbl_description, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "password-visible", priv->lbl_password, "visible", G_BINDING_DEFAULT); + + priv->password_buffer = gcr_secure_entry_buffer_new (); + gtk_entry_set_buffer (GTK_ENTRY (priv->entry_password), GTK_ENTRY_BUFFER (priv->password_buffer)); + g_object_bind_property (self, "password-visible", priv->entry_password, + "visible", G_BINDING_DEFAULT); + + g_object_bind_property (self, "confirm-visible", priv->lbl_confirm, "visible", G_BINDING_DEFAULT); + + priv->confirm_buffer = gcr_secure_entry_buffer_new (); + gtk_entry_set_buffer (GTK_ENTRY (priv->entry_confirm), GTK_ENTRY_BUFFER (priv->confirm_buffer)); + g_object_bind_property (self, "confirm-visible", priv->entry_confirm, "visible", G_BINDING_DEFAULT); + + g_object_bind_property (self, "confirm-visible", priv->pbar_quality, "visible", G_BINDING_DEFAULT); + g_signal_connect_swapped (priv->entry_password, "changed", + G_CALLBACK (on_password_changed), self); + + g_object_bind_property (self, "warning", priv->lbl_warning, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "warning-visible", priv->lbl_warning, "visible", G_BINDING_DEFAULT); + + g_object_bind_property (self, "choice-label", priv->lbl_choice, + "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "choice-visible", priv->lbl_choice, + "visible", G_BINDING_DEFAULT); + g_object_bind_property (self, "choice-visible", priv->checkbtn_choice, + "visible", G_BINDING_DEFAULT); + g_object_bind_property (self, "choice-chosen", priv->checkbtn_choice, + "active", G_BINDING_BIDIRECTIONAL); + + g_object_bind_property (self, "cancel-label", priv->btn_cancel, "label", G_BINDING_DEFAULT); + g_object_bind_property (self, "continue-label", priv->btn_continue, "label", G_BINDING_DEFAULT); + + gtk_widget_grab_default (priv->btn_continue); +} + + +static void +phosh_system_prompt_class_init (PhoshSystemPromptClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_system_prompt_get_property; + object_class->set_property = phosh_system_prompt_set_property; + object_class->constructed = phosh_system_prompt_constructed; + object_class->dispose = phosh_system_prompt_dispose; + object_class->finalize = phosh_system_prompt_finalize; + + g_object_class_override_property (object_class, PROP_MESSAGE, "message"); + g_object_class_override_property (object_class, PROP_DESCRIPTION, "description"); + g_object_class_override_property (object_class, PROP_WARNING, "warning"); + g_object_class_override_property (object_class, PROP_PASSWORD_NEW, "password-new"); + g_object_class_override_property (object_class, PROP_PASSWORD_STRENGTH, "password-strength"); + g_object_class_override_property (object_class, PROP_CHOICE_LABEL, "choice-label"); + g_object_class_override_property (object_class, PROP_CHOICE_CHOSEN, "choice-chosen"); + g_object_class_override_property (object_class, PROP_CALLER_WINDOW, "caller-window"); + g_object_class_override_property (object_class, PROP_CONTINUE_LABEL, "continue-label"); + g_object_class_override_property (object_class, PROP_CANCEL_LABEL, "cancel-label"); + + /** + * GcrPromptDialog:password-visible: + * + * Whether the password entry is visible or not. + */ + g_object_class_install_property (object_class, PROP_PASSWORD_VISIBLE, + g_param_spec_boolean ("password-visible", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * GcrPromptDialog:confirm-visible: + * + * Whether the password confirm entry is visible or not. + */ + g_object_class_install_property (object_class, PROP_CONFIRM_VISIBLE, + g_param_spec_boolean ("confirm-visible", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * PhoshSystemPrompt:warning-visible: + * + * Whether the warning label is visible or not. + */ + g_object_class_install_property (object_class, PROP_WARNING_VISIBLE, + g_param_spec_boolean ("warning-visible", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + /** + * PhoshSystemPrompt:choice-visible: + * + * Whether the choice check box is visible or not. + */ + g_object_class_install_property (object_class, PROP_CHOICE_VISIBLE, + g_param_spec_boolean ("choice-visible", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS)); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/system-prompt.ui"); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, grid); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, lbl_description); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, lbl_password); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, entry_password); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, lbl_confirm); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, entry_confirm); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, pbar_quality); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, lbl_warning); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, checkbtn_choice); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, lbl_choice); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, btn_cancel); + gtk_widget_class_bind_template_child_private (widget_class, PhoshSystemPrompt, btn_continue); + gtk_widget_class_bind_template_callback (widget_class, on_dialog_canceled); + gtk_widget_class_bind_template_callback (widget_class, on_btn_continue_clicked); +} + + +static void +phosh_system_prompt_init (PhoshSystemPrompt *self) +{ + /* + * This is a stupid hack to help the window act like a normal object + * with regards to reference counting and unref. (see gcr's + * gcr_prompt_dialog_init ui/gcr-prompt-dialog.c as of + * e0a506eeb29bc6be01a96e805e0244a03428ebf5. + * Otherwise it gets clean up too early. + */ + gtk_window_set_has_user_ref_count (GTK_WINDOW (self), FALSE); + gtk_widget_init_template (GTK_WIDGET (self)); +} + + +GtkWidget * +phosh_system_prompt_new (void) +{ + return g_object_new (PHOSH_TYPE_SYSTEM_PROMPT, NULL); +} diff --git a/src/system-prompt.h b/src/system-prompt.h new file mode 100644 index 000000000..059801f27 --- /dev/null +++ b/src/system-prompt.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "system-modal-dialog.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_SYSTEM_PROMPT (phosh_system_prompt_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshSystemPrompt, phosh_system_prompt, PHOSH, SYSTEM_PROMPT, PhoshSystemModalDialog) + +GtkWidget *phosh_system_prompt_new (void); + +G_END_DECLS diff --git a/src/system-prompter.c b/src/system-prompter.c new file mode 100644 index 000000000..fac2a979a --- /dev/null +++ b/src/system-prompter.c @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-system-prompter" + +#include "phosh-config.h" + +#include "system-prompt.h" +#include "system-prompter.h" +#include "shell-priv.h" +#include "phosh-wayland.h" + +/** + * PhoshSystemPrompter: + * + * Manages system prompter registration + * + * The PhoshSystemPrompter is responsible for displaying system + * wide modal #PhoshSystemPrompt dialogs + */ +static GcrSystemPrompter *_prompter; +static ulong owner_id; +static gboolean registered_prompter; +static gboolean acquired_prompter; + + +static GcrPrompt * +new_prompt_cb (GcrSystemPrompter *prompter, + gpointer user_data) +{ + GtkWidget *prompt; + + g_debug ("Building new system prompt"); + g_return_val_if_fail (GCR_IS_SYSTEM_PROMPTER (prompter), NULL); + + prompt = phosh_system_prompt_new (); + + return GCR_PROMPT (prompt); +} + + +static void +on_bus_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("bus acquired for %s", name); + + if (!registered_prompter) { + gcr_system_prompter_register (_prompter, connection); + g_debug ("registered prompter"); + } + registered_prompter = TRUE; +} + + +static void +on_name_lost (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("lost name: %s", name); + + /* Called like so when no connection can be made */ + if (connection == NULL) { + g_warning ("couldn't connect to session bus"); + phosh_system_prompter_unregister (); + registered_prompter = FALSE; + } +} + + +static void +on_name_acquired (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + g_debug ("acquired name: %s", name); + acquired_prompter = TRUE; +} + +/** + * phosh_system_prompter_register: + * + * Register the system prompter + * + * Returns:(transfer none): The system prompter + */ +GcrSystemPrompter * +phosh_system_prompter_register (void) +{ + _prompter = gcr_system_prompter_new (GCR_SYSTEM_PROMPTER_SINGLE, 0); + + g_signal_connect (_prompter, "new-prompt", + G_CALLBACK (new_prompt_cb), NULL); + + owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + "org.gnome.keyring.SystemPrompter", + G_BUS_NAME_OWNER_FLAGS_REPLACE, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + return _prompter; +} + + +void +phosh_system_prompter_unregister(void) +{ + if (_prompter) { + if (registered_prompter) { + gcr_system_prompter_unregister (_prompter, TRUE); + registered_prompter = FALSE; + } + g_clear_object (&_prompter); + } + + if (owner_id) { + g_bus_unown_name (owner_id); + owner_id = 0; + } + + acquired_prompter = FALSE; +} diff --git a/src/system-prompter.h b/src/system-prompter.h new file mode 100644 index 000000000..35a2db44d --- /dev/null +++ b/src/system-prompter.h @@ -0,0 +1,13 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#define GCR_API_SUBJECT_TO_CHANGE +#include + +GcrSystemPrompter *phosh_system_prompter_register(void); +void phosh_system_prompter_unregister(void); diff --git a/src/thumbnail-priv.h b/src/thumbnail-priv.h new file mode 100644 index 000000000..81233a84a --- /dev/null +++ b/src/thumbnail-priv.h @@ -0,0 +1,17 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#pragma once + +#include "thumbnail.h" + +G_BEGIN_DECLS + +void phosh_thumbnail_set_ready (PhoshThumbnail *self, gboolean ready); + +G_END_DECLS diff --git a/src/thumbnail.c b/src/thumbnail.c new file mode 100644 index 000000000..6daa6739d --- /dev/null +++ b/src/thumbnail.c @@ -0,0 +1,131 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Sebastian Krzyszkowiak + */ + +#define G_LOG_DOMAIN "phosh-thumbnail" + +#include "thumbnail-priv.h" + +/** + * PhoshThumbnail: + * + * An abstract class representing a thumbnail image. + */ + +enum { + PROP_0, + PROP_READY, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +typedef struct _PhoshThumbnailPrivate { + gboolean ready; +} PhoshThumbnailPrivate; + +G_DEFINE_TYPE_WITH_PRIVATE (PhoshThumbnail, phosh_thumbnail, G_TYPE_OBJECT); + + +static void +phosh_thumbnail_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshThumbnail *self = PHOSH_THUMBNAIL (object); + + switch (property_id) { + case PROP_READY: + g_value_set_boolean (value, phosh_thumbnail_is_ready (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_thumbnail_class_init (PhoshThumbnailClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->get_property = phosh_thumbnail_get_property; + + /** + * PhoshThumbnail:ready: + * + * Whether the image data is ready to be used + */ + props[PROP_READY] = + g_param_spec_boolean ("ready", "", "", + FALSE, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_thumbnail_init (PhoshThumbnail *self) +{ +} + + +gpointer +phosh_thumbnail_get_image (PhoshThumbnail *self) +{ + PhoshThumbnailClass *klass; + + g_return_val_if_fail (PHOSH_IS_THUMBNAIL (self), NULL); + + klass = PHOSH_THUMBNAIL_GET_CLASS (self); + g_return_val_if_fail (klass->get_image != NULL, NULL); + + return klass->get_image (self); +} + + +void +phosh_thumbnail_get_size (PhoshThumbnail *self, guint *width, guint *height, guint *stride) +{ + PhoshThumbnailClass *klass; + + g_return_if_fail (PHOSH_IS_THUMBNAIL (self)); + + klass = PHOSH_THUMBNAIL_GET_CLASS (self); + g_return_if_fail (klass->get_size != NULL); + + return klass->get_size (self, width, height, stride); +} + + +gboolean +phosh_thumbnail_is_ready (PhoshThumbnail *self) +{ + PhoshThumbnailPrivate *priv = phosh_thumbnail_get_instance_private (self); + + g_return_val_if_fail (PHOSH_IS_THUMBNAIL (self), FALSE); + + return priv->ready; +} + + +void +phosh_thumbnail_set_ready (PhoshThumbnail *self, gboolean ready) +{ + PhoshThumbnailPrivate *priv = phosh_thumbnail_get_instance_private (self); + + g_return_if_fail (PHOSH_IS_THUMBNAIL (self)); + + if (priv->ready == ready) + return; + + priv->ready = ready; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_READY]); + +} diff --git a/src/thumbnail.h b/src/thumbnail.h new file mode 100644 index 000000000..5b604574f --- /dev/null +++ b/src/thumbnail.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#define PHOSH_TYPE_THUMBNAIL (phosh_thumbnail_get_type ()) + +G_DECLARE_DERIVABLE_TYPE (PhoshThumbnail, phosh_thumbnail, PHOSH, THUMBNAIL, GObject) + +/** + * PhoshThumbnailClass: + * @parent_class: the parent class + * @get_image: Get the current image data + * @get_size: get current image size and stride + */ +struct _PhoshThumbnailClass { + GObjectClass parent_class; + gpointer (*get_image) (PhoshThumbnail *self); + void (*get_size) (PhoshThumbnail *self, guint *width, guint *height, guint *stride); +}; + +gpointer phosh_thumbnail_get_image (PhoshThumbnail *self); +void phosh_thumbnail_get_size (PhoshThumbnail *self, guint *width, guint *height, + guint *stride); +gboolean phosh_thumbnail_is_ready (PhoshThumbnail *self); diff --git a/src/top-panel-bg.c b/src/top-panel-bg.c new file mode 100644 index 000000000..f2fe64ba9 --- /dev/null +++ b/src/top-panel-bg.c @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-top-panel-bg" + +#include "phosh-config.h" + +#include "layersurface-priv.h" +#include "top-panel-bg.h" + +#define PHOSH_TOP_PANEL_BG_MAX_OPACITY 0.9 + +/** + * PhoshTopPanelBgBg: + * + * The (semi transparent) background of the top panel + */ +enum { + PROP_0, + PROP_MAX_OPACITY, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshTopPanelBg { + PhoshLayerSurface parent; + + double max_opacity; +}; +G_DEFINE_TYPE (PhoshTopPanelBg, phosh_top_panel_bg, PHOSH_TYPE_LAYER_SURFACE) + + +static void +phosh_top_panel_bg_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTopPanelBg *self = PHOSH_TOP_PANEL_BG (object); + + switch (property_id) { + case PROP_MAX_OPACITY: + g_value_set_double (value, self->max_opacity); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_top_panel_bg_class_init (PhoshTopPanelBgClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->get_property = phosh_top_panel_bg_get_property; + + props[PROP_MAX_OPACITY] = + g_param_spec_double ("max-opacity", "", "", + 0.0, 1.0, PHOSH_TOP_PANEL_BG_MAX_OPACITY, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + gtk_widget_class_set_css_name (widget_class, "phosh-top-panel-bg"); +} + + +static void +phosh_top_panel_bg_init (PhoshTopPanelBg *self) +{ + self->max_opacity = PHOSH_TOP_PANEL_BG_MAX_OPACITY; +} + + +PhoshTopPanelBg * +phosh_top_panel_bg_new (struct zwlr_layer_shell_v1 *layer_shell, + PhoshMonitor *monitor, + guint32 layer) + +{ + return g_object_new (PHOSH_TYPE_TOP_PANEL_BG, + "layer-shell", layer_shell, + "wl-output", monitor->wl_output, + "anchor", (ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_BOTTOM | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT), + "layer", layer, + "kbd-interactivity", FALSE, + "exclusive-zone", -1, + "namespace", "phosh top-panel background", + NULL); +} + +/** + * phosh_top_panel_bg_set_transparency: + * @self: The background + * @transparency: the transparency + * + * Set's the backgrounds transparency between 1.0 (fully transparent) and + * 0.0 (opaque). How opaque the surface actually is determined by + * [property@TopPanelBg:max-opacity]. + */ +void +phosh_top_panel_bg_set_transparency (PhoshTopPanelBg *self, double transparency) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL_BG (self)); + + phosh_layer_surface_set_alpha (PHOSH_LAYER_SURFACE (self), + self->max_opacity * (1.0 - transparency)); +} diff --git a/src/top-panel-bg.h b/src/top-panel-bg.h new file mode 100644 index 000000000..6ee56aea9 --- /dev/null +++ b/src/top-panel-bg.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "layersurface.h" +#include "monitor/monitor.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TOP_PANEL_BG (phosh_top_panel_bg_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTopPanelBg, phosh_top_panel_bg, PHOSH, TOP_PANEL_BG, PhoshLayerSurface) + +PhoshTopPanelBg *phosh_top_panel_bg_new (struct zwlr_layer_shell_v1 *layer_shell, + PhoshMonitor *monitor, + guint32 layer); +void phosh_top_panel_bg_set_transparency (PhoshTopPanelBg *self, + double transparency); + +G_END_DECLS diff --git a/src/top-panel.c b/src/top-panel.c new file mode 100644 index 000000000..e8da3a583 --- /dev/null +++ b/src/top-panel.c @@ -0,0 +1,1119 @@ +/* + * Copyright (C) 2018-2022 Purism SPC + * 2023-2024 The Phosh Developers + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + * + * Somewhat based on maynard's panel which is + * Copyright (C) 2014 Collabora Ltd. * + * Author: Jonny Lamb + */ + +#define G_LOG_DOMAIN "phosh-top-panel" + +#include "phosh-config.h" + +#include "layersurface-priv.h" +#include "layout-manager.h" +#include "shell-priv.h" +#include "session-manager.h" +#include "settings.h" +#include "top-panel.h" +#include "top-panel-bg.h" +#include "arrow.h" +#include "util.h" +#include "wall-clock.h" + +#include + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#include + +#define KEYBINDINGS_SCHEMA_ID "org.gnome.shell.keybindings" +#define KEYBINDING_KEY_TOGGLE_MESSAGE_TRAY "toggle-message-tray" + +#define PHOSH_TOP_PANEL_DRAG_THRESHOLD 0.3 + +/** + * PhoshTopPanel: + * + * The top panel + * + * The top panel containing the top-bar (clock and status indicators) and, when activated + * unfolds the #PhoshSettings. + */ +enum { + PROP_0, + PROP_TOP_PANEL_STATE, + PROP_ON_LOCKSCREEN, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + ACTIVATED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +typedef struct _PhoshTopPanel { + PhoshDragSurface parent; + + PhoshTopPanelState state; + gboolean on_lockscreen; + + GtkWidget *box; /* main content box */ + + /* Top row above settings */ + GtkWidget *btn_power; + GtkWidget *menu_system; + GtkWidget *box_clock; + GtkWidget *btn_lock; + GtkWidget *lbl_clock2; + GtkWidget *lbl_date; + + GtkWidget *stack; + GtkWidget *arrow; + GtkWidget *launch_settings_revealer; + + GtkWidget *top_bar_bin; + GtkWidget *box_top_bar; + GtkWidget *lbl_clock; /* top-bar clock */ + GtkWidget *lbl_lang; + + GtkWidget *settings; /* settings menu */ + GtkWidget *batteryinfo; + + GnomeXkbInfo *xkbinfo; + GSettings *input_settings; + GSettings *interface_settings; + GdkSeat *seat; + + GSimpleActionGroup *actions; + + /* Keybinding */ + GStrv action_names; + GSettings *kb_settings; + + GtkGesture *click_gesture; /* needed so that the gesture isn't destroyed immediately */ + + PhoshTopPanelBg *background; + GtkCssProvider *cutout_css_provider; + + GCancellable *cancel; +} PhoshTopPanel; + +G_DEFINE_TYPE (PhoshTopPanel, phosh_top_panel, PHOSH_TYPE_DRAG_SURFACE) + +static void +phosh_top_panel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (object); + + switch (property_id) { + case PROP_ON_LOCKSCREEN: + self->on_lockscreen = g_value_get_boolean (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_top_panel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (object); + + switch (property_id) { + case PROP_TOP_PANEL_STATE: + g_value_set_enum (value, self->state); + break; + case PROP_ON_LOCKSCREEN: + g_value_set_boolean (value, self->on_lockscreen); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +update_drag_handle (PhoshTopPanel *self, gboolean queue_draw) +{ + int handle, offset; + + /* By default only the bottom bar handle is draggable */ + handle = phosh_layer_surface_get_configured_height (PHOSH_LAYER_SURFACE (self)); + handle -= PHOSH_TOP_BAR_HEIGHT; + + /* Settings might enlarge the draggable area */ + offset = phosh_settings_get_drag_handle_offset (PHOSH_SETTINGS (self->settings)); + handle -= offset; + + g_debug ("Drag Handle: %d", handle); + if (handle < 0) + return; + + phosh_drag_surface_set_drag_mode (PHOSH_DRAG_SURFACE (self), + PHOSH_DRAG_SURFACE_DRAG_MODE_HANDLE); + phosh_drag_surface_set_drag_handle (PHOSH_DRAG_SURFACE (self), handle); + + /* Trigger redraw and surface commit */ + if (queue_draw) + gtk_widget_queue_draw (GTK_WIDGET (self)); +} + + +static void +on_settings_drag_handle_offset_changed (PhoshTopPanel *self, GParamSpec *pspec) +{ + update_drag_handle (self, TRUE); +} + + +static void +on_shutdown_action (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL(data); + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + phosh_session_manager_shutdown (sm); + phosh_top_panel_fold (self); +} + + +static void +on_restart_action (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL(data); + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (sm)); + + phosh_session_manager_reboot (sm); + phosh_top_panel_fold (self); +} + + +static void +on_suspend_action (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (data); + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_action_group_activate_action (G_ACTION_GROUP (phosh_shell_get_default ()), + "suspend.trigger-suspend", NULL); + phosh_top_panel_fold (self); +} + + +static void +on_lockscreen_action (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL(data); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + phosh_shell_lock (phosh_shell_get_default ()); + phosh_top_panel_fold (self); +} + + +static void +on_logout_action (GSimpleAction *action, + GVariant *parameter, + gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL(data); + PhoshSessionManager *sm = phosh_shell_get_session_manager (phosh_shell_get_default ()); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_return_if_fail (PHOSH_IS_SESSION_MANAGER (sm)); + phosh_session_manager_logout (sm); + phosh_top_panel_fold (self); +} + + +static void +on_open_panel_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (user_data); + g_autoptr (GError) err = NULL; + gboolean success; + + success = phosh_util_open_settings_panel_finish (res, &err); + if (!success) { + if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + /* TODO: show notification */ + g_warning ("Failed to open panel: %s", err->message); + return; + } + + phosh_top_panel_fold (self); +} + + +static void +open_settings_panel (PhoshTopPanel *self, gboolean mobile, const char *panel, GVariant *params) +{ + if (self->on_lockscreen) + return; + + phosh_util_open_settings_panel (panel, + params, + mobile, + self->cancel, + on_open_panel_ready, + self); +} + + +static void +parse_panel_options (GSimpleAction *action, GVariant *data, const char **panel, GVariant **params) +{ + g_assert (panel); + + if (!params) { + g_critical ("Failed to parse panel options: 'params' NULL"); + + return; + } + + g_variant_get (data, "(&s@av)", panel, params); +} + + +static void +on_launch_panel_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (data); + const char *panel; + g_autoptr (GVariant) params = NULL; + + parse_panel_options (action, param, &panel, ¶ms); + + open_settings_panel (self, FALSE, panel, params); + phosh_settings_hide_details (PHOSH_SETTINGS (self->settings)); +} + + +static void +on_launch_mobile_panel_activated (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (data); + const char *panel; + g_autoptr (GVariant) params = NULL; + + parse_panel_options (action, param, &panel, ¶ms); + + open_settings_panel (self, TRUE, panel, params); + phosh_settings_hide_details (PHOSH_SETTINGS (self->settings)); +} + + +static void +wall_clock_notify_cb (PhoshTopPanel *self, + GParamSpec *pspec, + PhoshWallClock *wall_clock) +{ + const char *str; + g_autofree char *date = NULL; + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_return_if_fail (PHOSH_IS_WALL_CLOCK (wall_clock)); + + str = phosh_wall_clock_get_clock (wall_clock, TRUE); + gtk_label_set_text (GTK_LABEL (self->lbl_clock2), str); + + date = phosh_wall_clock_local_date (wall_clock); + gtk_label_set_label (GTK_LABEL (self->lbl_date), date); +} + + +static gboolean +needs_keyboard_label (PhoshTopPanel *self) +{ + GList *slaves; + g_autoptr(GVariant) sources = NULL; + + g_return_val_if_fail (GDK_IS_SEAT (self->seat), FALSE); + g_return_val_if_fail (G_IS_SETTINGS (self->input_settings), FALSE); + + if (!phosh_shell_get_docked (phosh_shell_get_default ())) { + return FALSE; + } + + sources = g_settings_get_value(self->input_settings, "sources"); + if (g_variant_n_children (sources) < 2) + return FALSE; + + slaves = gdk_seat_get_slaves (self->seat, GDK_SEAT_CAPABILITY_KEYBOARD); + if (!slaves) + return FALSE; + + g_list_free (slaves); + return TRUE; +} + + +static gboolean +transform_docked_mode_to_lang_label_visible (GBinding *binding, + const GValue *from_value, + GValue *to_value, + gpointer data) +{ + gboolean visible = FALSE; + gboolean docked = g_value_get_boolean (from_value); + PhoshTopPanel *self = PHOSH_TOP_PANEL (data); + + if (docked && needs_keyboard_label (self)) + visible = TRUE; + + g_value_set_boolean (to_value, visible); + return TRUE; +} + + +static void +on_seat_device_changed (PhoshTopPanel *self, GdkDevice *device, GdkSeat *seat) +{ + gboolean visible; + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_return_if_fail (GDK_IS_SEAT (seat)); + + visible = needs_keyboard_label (self); + gtk_widget_set_visible (self->lbl_lang, visible); +} + + +static void +on_input_setting_changed (PhoshTopPanel *self, + const char *key, + GSettings *settings) +{ + g_autoptr(GVariant) sources = NULL; + gboolean visible; + GVariantIter iter; + g_autofree char *id = NULL; + g_autofree char *type = NULL; + const char *name; + + visible = needs_keyboard_label (self); + + sources = g_settings_get_value(settings, "sources"); + g_variant_iter_init (&iter, sources); + g_variant_iter_next (&iter, "(ss)", &type, &id); + + if (g_strcmp0 (type, "xkb")) { + g_debug ("Not a xkb layout: '%s' - ignoring", id); + goto out; + } + + if (!gnome_xkb_info_get_layout_info (self->xkbinfo, id, + NULL, &name, NULL, NULL)) { + g_debug ("Failed to get layout info for %s", id); + name = id; + } + g_debug ("Layout is %s", name); + gtk_label_set_text (GTK_LABEL (self->lbl_lang), name); + out: + gtk_widget_set_visible (self->lbl_lang, visible); +} + + +static void +released_cb (PhoshTopPanel *self, int n_press, double x, double y, GtkGestureMultiPress *gesture) +{ + /* + * The popover has to be popdown manually as it doesn't happen + * automatically when the power button is tapped with touch + */ + if (gtk_widget_is_visible (self->menu_system)) { + gtk_popover_popdown (GTK_POPOVER (self->menu_system)); + return; + } + + if (phosh_util_gesture_is_touch (GTK_GESTURE_SINGLE (gesture)) == FALSE) + g_signal_emit (self, signals[ACTIVATED], 0); +} + + +static void +toggle_message_tray_action (GSimpleAction *action, GVariant *param, gpointer data) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (data); + + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + + phosh_top_panel_toggle_fold (self); + /* TODO: focus message tray */ +} + + +static void +add_keybindings (PhoshTopPanel *self) +{ + g_autoptr (GStrvBuilder) builder = g_strv_builder_new (); + g_autoptr (GArray) actions = g_array_new (FALSE, TRUE, sizeof (GActionEntry)); + + PHOSH_UTIL_BUILD_KEYBINDING (actions, + builder, + self->kb_settings, + KEYBINDING_KEY_TOGGLE_MESSAGE_TRAY, + toggle_message_tray_action); + + phosh_shell_add_global_keyboard_action_entries (phosh_shell_get_default (), + (GActionEntry*) actions->data, + actions->len, + self); + self->action_names = g_strv_builder_end (builder); +} + + +static void +on_keybindings_changed (PhoshTopPanel *self, + char *key, + GSettings *settings) +{ + /* For now just redo all keybindings */ + g_debug ("Updating keybindings"); + phosh_shell_remove_global_keyboard_action_entries (phosh_shell_get_default (), + self->action_names); + g_clear_pointer (&self->action_names, g_strfreev); + add_keybindings (self); +} + + +static double +ease_in_quintic (double p) +{ + return p * p * p * p * p; +} + + +static void +phosh_top_panel_dragged (PhoshDragSurface *drag_surface, int margin) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (drag_surface); + int width, height; + double progress, transparency; + + gtk_window_get_size (GTK_WINDOW (self), &width, &height); + progress = -margin / (double)(height - PHOSH_TOP_BAR_HEIGHT); + phosh_arrow_set_progress (PHOSH_ARROW (self->arrow), progress); + + progress = MIN (1.0, progress); + transparency = ease_in_quintic (progress); + phosh_top_panel_bg_set_transparency (self->background, transparency); +} + + +static void +on_drag_state_changed (PhoshTopPanel *self) +{ + PhoshTopPanelState state = self->state; + const char *visible; + gboolean kbd_interactivity = FALSE; + double progress = -1.0; + + /* Close the popover on any drag */ + gtk_widget_set_visible (self->menu_system, FALSE); + + switch (phosh_drag_surface_get_drag_state (PHOSH_DRAG_SURFACE (self))) { + case PHOSH_DRAG_SURFACE_STATE_UNFOLDED: + state = PHOSH_TOP_PANEL_STATE_UNFOLDED; + update_drag_handle (self, TRUE); + kbd_interactivity = TRUE; + visible = "arrow"; + progress = 0.0; + phosh_top_panel_bg_set_transparency (self->background, 0.0); + break; + case PHOSH_DRAG_SURFACE_STATE_FOLDED: + state = PHOSH_TOP_PANEL_STATE_FOLDED; + visible = "top-bar"; + progress = 1.0; + phosh_top_panel_bg_set_transparency (self->background, 1.0); + break; + case PHOSH_DRAG_SURFACE_STATE_DRAGGED: + visible = "arrow"; + progress = phosh_arrow_get_progress (PHOSH_ARROW (self->arrow)); + break; + default: + g_return_if_reached (); + } + + g_debug ("%s: state: %d, visible: %s", __func__, self->state, visible); + gtk_stack_set_visible_child_name (GTK_STACK (self->stack), visible); + phosh_arrow_set_progress (PHOSH_ARROW (self->arrow), progress); + + if (self->state != state) { + self->state = state; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TOP_PANEL_STATE]); + } + + if (self->state == PHOSH_TOP_PANEL_STATE_FOLDED) + phosh_settings_hide_details (PHOSH_SETTINGS (self->settings)); + + gtk_revealer_set_reveal_child (GTK_REVEALER (self->launch_settings_revealer), + progress <= 0.0); + + phosh_layer_surface_set_kbd_interactivity (PHOSH_LAYER_SURFACE (self), kbd_interactivity); + phosh_layer_surface_wl_surface_commit (PHOSH_LAYER_SURFACE (self)); +} + + +static void +phosh_top_panel_map (GtkWidget *widget) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (widget); + + GTK_WIDGET_CLASS (phosh_top_panel_parent_class)->map (widget); + + phosh_layer_surface_set_stacked_below (PHOSH_LAYER_SURFACE (self->background), + PHOSH_LAYER_SURFACE (self)); +} + + +static void +phosh_top_panel_add_background (PhoshTopPanel *self) +{ + PhoshWayland *wl = phosh_wayland_get_default (); + PhoshShell *shell = phosh_shell_get_default (); + PhoshMonitor *monitor; + guint32 layer; + cairo_rectangle_int_t rect = { 0, 0, 0, 0 }; + cairo_region_t *region; + + monitor = phosh_shell_get_primary_monitor (shell); + layer = phosh_layer_surface_get_layer (PHOSH_LAYER_SURFACE (self)); + self->background = phosh_top_panel_bg_new (phosh_wayland_get_zwlr_layer_shell_v1 (wl), + monitor, + layer); + g_object_bind_property (self, "visible", self->background, "visible", G_BINDING_SYNC_CREATE); + + region = cairo_region_create_rectangle (&rect); + gtk_widget_input_shape_combine_region (GTK_WIDGET (self->background), region); + cairo_region_destroy (region); +} + + +static void +on_theme_name_changed (PhoshTopPanel *self, GParamSpec *pspec, PhoshStyleManager *style_manager) +{ + gboolean solid; + + g_assert (PHOSH_IS_TOP_PANEL (self)); + g_assert (PHOSH_IS_STYLE_MANAGER (style_manager)); + + solid = phosh_style_manager_is_high_contrast (style_manager); + phosh_util_toggle_style_class (GTK_WIDGET (self), "p-solid", solid); +} + + +static GActionEntry entries[] = { + { .name = "poweroff", .activate = on_shutdown_action }, + { .name = "restart", .activate = on_restart_action }, + { .name = "suspend", .activate = on_suspend_action }, + { .name = "lockscreen", .activate = on_lockscreen_action }, + { .name = "logout", .activate = on_logout_action }, + { .name = "launch-panel", .activate = on_launch_panel_activated, .parameter_type = "(sav)" }, + { .name = "launch-mobile-panel", + .activate = on_launch_mobile_panel_activated, + .parameter_type = "(sav)" }, +}; + + +static void +phosh_top_panel_constructed (GObject *object) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (object); + GdkDisplay *display = gdk_display_get_default (); + PhoshWallClock *wall_clock = phosh_wall_clock_get_default (); + PhoshShell *shell = phosh_shell_get_default (); + GAction *action; + + g_autoptr (GSettings) phosh_settings = g_settings_new ("sm.puri.phosh"); + + G_OBJECT_CLASS (phosh_top_panel_parent_class)->constructed (object); + + g_object_bind_property (phosh_shell_get_default (), "locked", + self, "on-lockscreen", + G_BINDING_SYNC_CREATE); + + g_object_bind_property (wall_clock, "date-time", + self->lbl_clock, "label", + G_BINDING_SYNC_CREATE); + g_object_bind_property (self, "on-lockscreen", + self->lbl_clock, + "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + g_signal_connect_object (wall_clock, + "notify::time", + G_CALLBACK (wall_clock_notify_cb), + self, + G_CONNECT_SWAPPED); + + wall_clock_notify_cb (self, NULL, wall_clock); + + gtk_window_set_title (GTK_WINDOW (self), "phosh panel"); + + /* language indicator */ + if (display) { + self->input_settings = g_settings_new ("org.gnome.desktop.input-sources"); + self->xkbinfo = gnome_xkb_info_new (); + self->seat = gdk_display_get_default_seat (display); + g_object_connect (self->seat, + "swapped-signal::device-added", G_CALLBACK (on_seat_device_changed), self, + "swapped-signal::device-removed", G_CALLBACK (on_seat_device_changed), self, + NULL); + g_signal_connect_swapped (self->input_settings, + "changed::sources", G_CALLBACK (on_input_setting_changed), + self); + on_input_setting_changed (self, NULL, self->input_settings); + + g_object_bind_property_full (phosh_shell_get_default (), + "docked", + self->lbl_lang, + "visible", + G_BINDING_SYNC_CREATE, + transform_docked_mode_to_lang_label_visible, + NULL, self, NULL); + } + + self->actions = g_simple_action_group_new (); + gtk_widget_insert_action_group (GTK_WIDGET (self), "panel", + G_ACTION_GROUP (self->actions)); + g_action_map_add_action_entries (G_ACTION_MAP (self->actions), + entries, G_N_ELEMENTS (entries), + self); + if (!phosh_shell_started_by_display_manager (phosh_shell_get_default ())) { + action = g_action_map_lookup_action (G_ACTION_MAP (self->actions), "logout"); + g_simple_action_set_enabled (G_SIMPLE_ACTION (action), FALSE); + } + + g_settings_bind (phosh_settings, + "enable-suspend", + g_action_map_lookup_action (G_ACTION_MAP (self->actions), "suspend"), + "enabled", + G_SETTINGS_BIND_GET); + + action = g_action_map_lookup_action (G_ACTION_MAP (self->actions), "launch-panel"); + g_object_bind_property (self, "on-lockscreen", + action, "enabled", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + action = g_action_map_lookup_action (G_ACTION_MAP (self->actions), "launch-mobile-panel"); + g_object_bind_property (self, "on-lockscreen", + action, "enabled", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + g_object_bind_property (self, "on-lockscreen", + self->launch_settings_revealer, + "visible", + G_BINDING_SYNC_CREATE | G_BINDING_INVERT_BOOLEAN); + + self->interface_settings = g_settings_new ("org.gnome.desktop.interface"); + g_settings_bind (self->interface_settings, + "show-battery-percentage", + self->batteryinfo, + "show-detail", + G_SETTINGS_BIND_GET); + + g_signal_connect_swapped (self->kb_settings, + "changed::" KEYBINDING_KEY_TOGGLE_MESSAGE_TRAY, + G_CALLBACK (on_keybindings_changed), + self); + add_keybindings (self); + + g_signal_connect (self, "notify::drag-state", G_CALLBACK (on_drag_state_changed), NULL); + + phosh_top_panel_add_background (self); + g_signal_connect_object (phosh_shell_get_style_manager (shell), + "notify::theme-name", + G_CALLBACK (on_theme_name_changed), + self, + G_CONNECT_SWAPPED); + on_theme_name_changed (self, NULL, phosh_shell_get_style_manager (shell)); +} + + +static void +phosh_top_panel_dispose (GObject *object) +{ + PhoshTopPanel *self = PHOSH_TOP_PANEL (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->kb_settings); + g_clear_object (&self->xkbinfo); + g_clear_object (&self->input_settings); + g_clear_object (&self->interface_settings); + g_clear_object (&self->actions); + g_clear_pointer (&self->action_names, g_strfreev); + if (self->seat) { + /* language indicator */ + g_signal_handlers_disconnect_by_data (self->seat, self); + self->seat = NULL; + } + g_clear_pointer (&self->background, phosh_cp_widget_destroy); + g_clear_object (&self->cutout_css_provider); + + G_OBJECT_CLASS (phosh_top_panel_parent_class)->dispose (object); +} + + +static int +get_margin (gint height) +{ + return (-1 * height) + PHOSH_TOP_BAR_HEIGHT; +} + + +static gboolean +on_configure_event (PhoshTopPanel *self, GdkEventConfigure *event) +{ + guint margin; + + margin = get_margin (event->height); + + /* ignore popovers like the power menu */ + if (gtk_widget_get_window (GTK_WIDGET (self)) != event->window) + return FALSE; + + g_debug ("%s: %dx%d margin: %d", __func__, event->height, event->width, margin); + + /* If the size changes we need to update the folded margin */ + phosh_drag_surface_set_margin (PHOSH_DRAG_SURFACE (self), margin, 0); + /* Update drag handle since top-panel size might have changed */ + update_drag_handle (self, TRUE); + + return FALSE; +} + + +static void +phosh_top_panel_configured (PhoshLayerSurface *layer_surface) +{ + guint width, height; + + width = phosh_layer_surface_get_configured_width (layer_surface); + height = phosh_layer_surface_get_configured_height (layer_surface); + + g_debug ("%s: %dx%d", __func__, width, height); + + PHOSH_LAYER_SURFACE_CLASS (phosh_top_panel_parent_class)->configured (layer_surface); +} + + +static void +phosh_top_panel_class_init (PhoshTopPanelClass *klass) +{ + GObjectClass *object_class = (GObjectClass *)klass; + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + PhoshLayerSurfaceClass *layer_surface_class = PHOSH_LAYER_SURFACE_CLASS (klass); + PhoshDragSurfaceClass *drag_surface_class = PHOSH_DRAG_SURFACE_CLASS (klass); + GtkBindingSet *binding_set; + + object_class->constructed = phosh_top_panel_constructed; + object_class->dispose = phosh_top_panel_dispose; + object_class->set_property = phosh_top_panel_set_property; + object_class->get_property = phosh_top_panel_get_property; + + widget_class->map = phosh_top_panel_map; + + layer_surface_class->configured = phosh_top_panel_configured; + + drag_surface_class->dragged = phosh_top_panel_dragged; + + /** + * PhoshTopPanel:state: + * + * Whether the top-panel is currently `folded` (only top-bar is + * visible) or `unfolded` (settings and notification area are + * visible). The property is updated when the widget reaches its + * target state. + */ + props[PROP_TOP_PANEL_STATE] = + g_param_spec_enum ("state", "", "", + PHOSH_TYPE_TOP_PANEL_STATE, + PHOSH_TOP_PANEL_STATE_UNFOLDED, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY | G_PARAM_STATIC_STRINGS); + /** + * PhoshTopPanel:on-lockscreen: + * + * Whether top-panel is shown on lockscreen (%TRUE) or in the unlocked shell + * (%FALSE). + * + * Consider this property to be read only. It's only r/w so we can + * use a property binding with the [type@Shell]s "locked" property. + */ + props[PROP_ON_LOCKSCREEN] = + g_param_spec_boolean ("on-lockscreen", "", "", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + signals[ACTIVATED] = g_signal_new ("activated", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 0); + + g_type_ensure (PHOSH_TYPE_ARROW); + + gtk_widget_class_set_template_from_resource (widget_class, + "/mobi/phosh/ui/top-panel.ui"); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, arrow); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, batteryinfo); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, box); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, box_clock); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, box_top_bar); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, btn_lock); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, btn_power); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, click_gesture); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, launch_settings_revealer); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, lbl_clock); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, lbl_clock2); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, lbl_date); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, lbl_lang); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, menu_system); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, settings); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, stack); + gtk_widget_class_bind_template_child (widget_class, PhoshTopPanel, top_bar_bin); + gtk_widget_class_bind_template_callback (widget_class, on_settings_drag_handle_offset_changed); + gtk_widget_class_bind_template_callback (widget_class, phosh_top_panel_fold); + gtk_widget_class_bind_template_callback (widget_class, released_cb); + + binding_set = gtk_binding_set_by_class (klass); + gtk_binding_entry_add_signal (binding_set, GDK_KEY_Escape, 0, "activated", 0); + + gtk_widget_class_set_css_name (widget_class, "phosh-top-panel"); +} + + +static void +set_clock_position (PhoshTopPanel *self, PhoshLayoutManager *layout_manager) +{ + PhoshLayoutClockPosition pos; + guint top_margin = 0; + + pos = phosh_layout_manager_get_clock_pos (layout_manager); + + /* Top-bar clock */ + gtk_container_remove (GTK_CONTAINER (self->box_top_bar), self->lbl_clock); + phosh_util_toggle_style_class (self->lbl_clock, "left", FALSE); + phosh_util_toggle_style_class (self->lbl_clock, "right", FALSE); + + switch (pos) { + case PHOSH_LAYOUT_CLOCK_POS_LEFT: + gtk_box_pack_start (GTK_BOX (self->box_top_bar), self->lbl_clock, FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (self->box_top_bar), self->lbl_clock, 0); + phosh_util_toggle_style_class (self->lbl_clock, "left", TRUE); + top_margin = phosh_layout_manager_get_clock_shift (layout_manager); + break; + case PHOSH_LAYOUT_CLOCK_POS_RIGHT: + gtk_box_pack_end (GTK_BOX (self->box_top_bar), self->lbl_clock, FALSE, FALSE, 0); + gtk_box_reorder_child (GTK_BOX (self->box_top_bar), self->lbl_clock, 1); + phosh_util_toggle_style_class (self->lbl_clock, "right", TRUE); + top_margin = phosh_layout_manager_get_clock_shift (layout_manager); + break; + case PHOSH_LAYOUT_CLOCK_POS_CENTER: + gtk_box_set_center_widget (GTK_BOX (self->box_top_bar), self->lbl_clock); + break; + default: + g_assert_not_reached (); + } + + /* Shift down settings menu clock */ + gtk_widget_set_margin_top (self->box_clock, top_margin); +} + + +static void +set_margin (PhoshTopPanel *self, PhoshLayoutManager *layout_manager) +{ + guint network_box_shift = 0, indicators_box_shift = 0, launch_settings_revealer_shift = 0; + g_autofree char *css = NULL; + g_autoptr (GtkCssProvider) provider = gtk_css_provider_new (); + + phosh_layout_manager_get_box_shifts (layout_manager, + &network_box_shift, + &indicators_box_shift, + &launch_settings_revealer_shift); + + g_debug ("Shifting UI elements %u,%u pixels to center ", + network_box_shift, + indicators_box_shift); + + css = g_strdup_printf ("#top-bar > :first-child {" + " padding-left: %dpx;" + "}" + "#top-bar > :last-child {" + " padding-right: %dpx;" + "}" + "#settings-gear {" + " padding-right: %dpx;" + "}", + network_box_shift, indicators_box_shift, launch_settings_revealer_shift); + gtk_css_provider_load_from_data (provider, css, -1, NULL); + gtk_style_context_add_provider_for_screen (gdk_screen_get_default (), + GTK_STYLE_PROVIDER (provider), + GTK_STYLE_PROVIDER_PRIORITY_APPLICATION + 1); + g_set_object (&self->cutout_css_provider, provider); +} + + +static void +on_layout_changed (PhoshTopPanel *self, PhoshLayoutManager *layout_manager) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + g_return_if_fail (PHOSH_IS_LAYOUT_MANAGER (layout_manager)); + + set_clock_position (self, layout_manager); + set_margin (self, layout_manager); +} + + +static void +phosh_top_panel_init (PhoshTopPanel *self) +{ + PhoshLayoutManager *layout_manager; + + gtk_widget_init_template (GTK_WIDGET (self)); + + self->cancel = g_cancellable_new (); + self->state = PHOSH_TOP_PANEL_STATE_UNFOLDED; + self->kb_settings = g_settings_new (KEYBINDINGS_SCHEMA_ID); + g_signal_connect (self, "configure-event", G_CALLBACK (on_configure_event), NULL); + + layout_manager = phosh_shell_get_layout_manager (phosh_shell_get_default ()); + g_signal_connect_object (layout_manager, + "layout-changed", + G_CALLBACK (on_layout_changed), + self, + G_CONNECT_SWAPPED); + on_layout_changed (self, layout_manager); +} + + +GtkWidget * +phosh_top_panel_new (struct zwlr_layer_shell_v1 *layer_shell, + struct zphoc_layer_shell_effects_v1 *layer_shell_effects, + PhoshMonitor *monitor, + guint32 layer) +{ + return g_object_new (PHOSH_TYPE_TOP_PANEL, + /* layer-surface */ + "layer-shell", layer_shell, + "wl-output", monitor->wl_output, + "height", PHOSH_TOP_BAR_HEIGHT, + "anchor", ZWLR_LAYER_SURFACE_V1_ANCHOR_TOP | + ZWLR_LAYER_SURFACE_V1_ANCHOR_LEFT | + ZWLR_LAYER_SURFACE_V1_ANCHOR_RIGHT, + "layer", layer, + "kbd-interactivity", FALSE, + "namespace", "phosh top-panel", + /* drag-surface */ + "layer-shell-effects", layer_shell_effects, + "exclusive", PHOSH_TOP_BAR_HEIGHT, + "threshold", PHOSH_TOP_PANEL_DRAG_THRESHOLD, + NULL); +} + + +void +phosh_top_panel_fold (PhoshTopPanel *self) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + + if (self->state == PHOSH_TOP_PANEL_STATE_FOLDED) + return; + + phosh_drag_surface_set_drag_state (PHOSH_DRAG_SURFACE (self), + PHOSH_DRAG_SURFACE_STATE_FOLDED); +} + + +void +phosh_top_panel_unfold (PhoshTopPanel *self) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + + if (self->state == PHOSH_TOP_PANEL_STATE_UNFOLDED) + return; + + phosh_drag_surface_set_drag_state (PHOSH_DRAG_SURFACE (self), + PHOSH_DRAG_SURFACE_STATE_UNFOLDED); +} + + +void +phosh_top_panel_toggle_fold (PhoshTopPanel *self) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + + if (self->state == PHOSH_TOP_PANEL_STATE_UNFOLDED) { + phosh_top_panel_fold (self); + } else { + phosh_top_panel_unfold (self); + } +} + + +PhoshTopPanelState +phosh_top_panel_get_state (PhoshTopPanel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOP_PANEL (self), PHOSH_TOP_PANEL_STATE_FOLDED); + + return self->state; +} + + +void +phosh_top_panel_set_layer (PhoshTopPanel *self, guint32 layer) +{ + g_assert (PHOSH_IS_TOP_PANEL (self)); + + phosh_layer_surface_set_layer (PHOSH_LAYER_SURFACE (self), layer); + phosh_layer_surface_wl_surface_commit (PHOSH_LAYER_SURFACE (self)); + + phosh_layer_surface_set_layer (PHOSH_LAYER_SURFACE (self->background), layer); + phosh_layer_surface_wl_surface_commit (PHOSH_LAYER_SURFACE (self->background)); +} + + +void +phosh_top_panel_set_bar_transparent (PhoshTopPanel *self, gboolean transparent) +{ + g_return_if_fail (PHOSH_IS_TOP_PANEL (self)); + + phosh_util_toggle_style_class (GTK_WIDGET (self->top_bar_bin), "p-solid", !transparent); +} diff --git a/src/top-panel.h b/src/top-panel.h new file mode 100644 index 000000000..1e4748ad5 --- /dev/null +++ b/src/top-panel.h @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "drag-surface.h" +#include "top-panel-bg.h" + +#define PHOSH_TYPE_TOP_PANEL (phosh_top_panel_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTopPanel, phosh_top_panel, PHOSH, TOP_PANEL, PhoshDragSurface) + +#define PHOSH_TOP_BAR_HEIGHT 32 +#define PHOSH_TOP_BAR_ICON_PIXEL_SIZE 16 +/* Minimum padding of network and indicator box to the left / right screen edge */ +#define PHOSH_TOP_BAR_MIN_PADDING 9 +/* Minimum padding of network and indicator box to the left / right display cutouts */ +#define PHOSH_TOP_BAR_CUTOUT_MIN_PADDING (PHOSH_TOP_BAR_MIN_PADDING / 2) + +/** + * PhoshTopPanelState: + * @PHOSH_TOP_PANEL_STATE_FOLDED: Only top-bar is visible + * @PHOSH_TOP_PANEL_STATE_UNFOLDED: Settings menu is unfolded + */ +typedef enum { + PHOSH_TOP_PANEL_STATE_FOLDED, + PHOSH_TOP_PANEL_STATE_UNFOLDED, +} PhoshTopPanelState; + +GtkWidget *phosh_top_panel_new (struct zwlr_layer_shell_v1 *layer_shell, + struct zphoc_layer_shell_effects_v1 *layer_shell_effects, + PhoshMonitor *monitor, + guint32 layer); +void phosh_top_panel_toggle_fold (PhoshTopPanel *self); +void phosh_top_panel_fold (PhoshTopPanel *self); +void phosh_top_panel_unfold (PhoshTopPanel *self); +PhoshTopPanelState phosh_top_panel_get_state (PhoshTopPanel *self); +void phosh_top_panel_set_layer (PhoshTopPanel *self, guint32 layer); +void phosh_top_panel_set_bar_transparent (PhoshTopPanel *self, gboolean transparent); diff --git a/src/toplevel-manager.c b/src/toplevel-manager.c new file mode 100644 index 000000000..b15286bed --- /dev/null +++ b/src/toplevel-manager.c @@ -0,0 +1,461 @@ +/* + * Copyright (C) 2019 Purism SPC + * 2023-2026 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Sebastian Krzyszkowiak + */ + +#define G_LOG_DOMAIN "phosh-toplevel-manager" + +#include "toplevel-manager.h" +#include "toplevel.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "util.h" +#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +#include + +/** + * PhoshToplevelManager: + * + * Tracks and interacts with toplevel surfaces for window management + * purposes using the wlr-foreign-toplevel-unstable-v1 wayland + * protocol. + */ + +enum { + PROP_0, + PROP_NUM_TOPLEVELS, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +enum { + TOPLEVEL_ADDED, + TOPLEVEL_CHANGED, + TOPLEVEL_MISSING, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +#define MAX_INITIAL_TOPLEVEL_TIMEOUT 30 /* s */ + +typedef struct { + GAppInfo *app_info; + guint timeout_id; + PhoshToplevelManager *manager; /* unowned */ +} LaunchingAppInfo; + +struct _PhoshToplevelManager { + GObject parent; + + GPtrArray *toplevels; /* (element-type: PhoshToplevel) */ + GPtrArray *toplevels_pending; /* (element-type: PhoshToplevel) */ + + PhoshAppTracker *app_tracker; + GPtrArray *launching_apps; /* (element-type: LaunchingToplevelInfo */ +}; + +G_DEFINE_TYPE (PhoshToplevelManager, phosh_toplevel_manager, G_TYPE_OBJECT); + + +static void +on_initial_toplevel_timeout (gpointer data) +{ + LaunchingAppInfo *info = data; + + g_signal_emit (info->manager, signals[TOPLEVEL_MISSING], 0, info->app_info); + info->timeout_id = 0; + + if (!g_ptr_array_remove_fast (info->manager->launching_apps, info)) + g_critical ("Failed to find launching info for %s", g_app_info_get_id (info->app_info)); +} + + +static void +launching_app_info_free (LaunchingAppInfo *info) +{ + g_clear_handle_id (&info->timeout_id, g_source_remove); + g_clear_object (&info->app_info); + g_free (info); +} + + +static LaunchingAppInfo * +launching_app_info_new (PhoshToplevelManager *toplevel_manager, GAppInfo *app_info) +{ + LaunchingAppInfo *info = g_new0 (LaunchingAppInfo, 1); + + info->app_info = g_object_ref (app_info); + info->manager = toplevel_manager; + + info->timeout_id = g_timeout_add_seconds_once (MAX_INITIAL_TOPLEVEL_TIMEOUT, + on_initial_toplevel_timeout, + info); + return info; +} + + +static gboolean +app_info_has_toplevel (PhoshToplevelManager *self, GAppInfo *app_info) +{ + const char *app_id = g_app_info_get_id (app_info); + + for (int i = 0; i < self->toplevels->len; i++) { + PhoshToplevel *toplevel = g_ptr_array_index (self->toplevels, i); + + if (g_strcmp0 (phosh_toplevel_get_app_id (toplevel), app_id) == 0) + return TRUE; + } + + return FALSE; +} + + +static void +remove_from_launching (PhoshToplevelManager *self, PhoshToplevel *toplevel) +{ + g_autoptr (GAppInfo) needle = NULL; + const char *app_id; + + app_id = phosh_toplevel_get_app_id (toplevel); + if (app_id) + return; + + needle = G_APP_INFO (phosh_get_desktop_app_info_for_app_id (app_id)); + if (!needle) + return; + + for (int i = 0; i < self->launching_apps->len; i++) { + LaunchingAppInfo *info = g_ptr_array_index (self->launching_apps, i); + + if (g_app_info_equal (G_APP_INFO (needle), info->app_info)) { + g_ptr_array_remove_index_fast (self->launching_apps, i); + g_debug ("Found toplevel for launching app %s", g_app_info_get_id (needle)); + return; + } + } + + g_debug ("Couldn't find app info toplevel %s", app_id); +} + + +static void +on_app_launched (PhoshToplevelManager *self, GAppInfo *app_info, const char *startup_id) +{ + LaunchingAppInfo *info; + const char *app_id = g_app_info_get_id (app_info); + + if (app_info_has_toplevel (self, app_info)) { + g_debug ("App info %s already has a toplevel", app_id); + return; + } + + g_warning ("Tracking %s as there's no toplevel yet", app_id); + info = launching_app_info_new (self, app_info); + g_ptr_array_add (self->launching_apps, info); +} + + +static void +phosh_toplevel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshToplevelManager *self = PHOSH_TOPLEVEL_MANAGER (object); + + switch (property_id) { + case PROP_NUM_TOPLEVELS: + g_value_set_int (value, self->toplevels->len); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + +static void +on_toplevel_closed (PhoshToplevelManager *self, PhoshToplevel *toplevel) +{ + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + /* Check if toplevel exists in toplevels_pending, in that case it is + * not yet configured and we just remove it from toplevels_pending + * without touching the regular toplevels array. */ + if (g_ptr_array_find (self->toplevels_pending, toplevel, NULL)) { + g_assert_true (g_ptr_array_remove (self->toplevels_pending, toplevel)); + g_object_unref (toplevel); + return; + } + + g_assert_true (g_ptr_array_remove (self->toplevels, toplevel)); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NUM_TOPLEVELS]); +} + + +static void +on_toplevel_configured (PhoshToplevelManager *self, GParamSpec *pspec, PhoshToplevel *toplevel) +{ + gboolean configured; + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self)); + g_return_if_fail (PHOSH_IS_TOPLEVEL (toplevel)); + + configured = phosh_toplevel_is_configured (toplevel); + + if (!configured) + return; + + if (g_ptr_array_find (self->toplevels, toplevel, NULL)) { + g_signal_emit (self, signals[TOPLEVEL_CHANGED], 0, toplevel); + } else { + g_assert_true (g_ptr_array_remove (self->toplevels_pending, toplevel)); + g_ptr_array_add (self->toplevels, toplevel); + g_signal_emit (self, signals[TOPLEVEL_ADDED], 0, toplevel); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NUM_TOPLEVELS]); + + remove_from_launching (self, toplevel); + } +} + + +static void +handle_zwlr_foreign_toplevel_manager_toplevel (void *data, + struct zwlr_foreign_toplevel_manager_v1 *unused, + struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + PhoshToplevelManager *self = data; + PhoshToplevel *toplevel; + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self)); + toplevel = phosh_toplevel_new_from_handle (handle); + + g_ptr_array_add (self->toplevels_pending, toplevel); + + g_signal_connect_swapped (toplevel, "closed", G_CALLBACK (on_toplevel_closed), self); + g_signal_connect_swapped (toplevel, + "notify::configured", + G_CALLBACK (on_toplevel_configured), + self); + + g_debug ("Got toplevel %p", toplevel); +} + + +static void +handle_zwlr_foreign_toplevel_manager_finished (void *data, + struct zwlr_foreign_toplevel_manager_v1 *unused) +{ + g_debug ("wlr_foreign_toplevel_manager_finished"); +} + + +static const +struct zwlr_foreign_toplevel_manager_v1_listener zwlr_foreign_toplevel_manager_listener = { + handle_zwlr_foreign_toplevel_manager_toplevel, + handle_zwlr_foreign_toplevel_manager_finished, +}; + + +static void +phosh_toplevel_manager_dispose (GObject *object) +{ + PhoshToplevelManager *self = PHOSH_TOPLEVEL_MANAGER (object); + if (self->toplevels) { + g_ptr_array_free (self->toplevels, TRUE); + self->toplevels = NULL; + } + if (self->toplevels_pending) { + g_ptr_array_free (self->toplevels_pending, TRUE); + self->toplevels_pending = NULL; + } + + g_clear_pointer (&self->launching_apps, g_ptr_array_unref); + g_clear_object (&self->app_tracker); + + G_OBJECT_CLASS (phosh_toplevel_manager_parent_class)->dispose (object); +} + + +static void +phosh_toplevel_manager_class_init (PhoshToplevelManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->dispose = phosh_toplevel_manager_dispose; + object_class->get_property = phosh_toplevel_get_property; + + /** + * PhoshToplevelManager:num-toplevels: + * + * The current number of toplevels + */ + props[PROP_NUM_TOPLEVELS] = + g_param_spec_int ("num-toplevels", "", "", + 0, G_MAXINT, 0, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + /** + * PhoshToplevelManager::toplevel-added: + * @manager: The #PhoshToplevelManager emitting the signal. + * @toplevel: The #PhoshToplevel being added to the list. + * + * Emitted whenever a toplevel has been added to the list. + */ + signals[TOPLEVEL_ADDED] = g_signal_new ("toplevel-added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + PHOSH_TYPE_TOPLEVEL); + /** + * PhoshToplevelManager::toplevel-changed: + * @manager: The #PhoshToplevelManager emitting the signal. + * @toplevel: The #PhoshToplevel that changed properties. + * + * Emitted whenever a toplevel has changed properties. + */ + signals[TOPLEVEL_CHANGED] = g_signal_new ("toplevel-changed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + PHOSH_TYPE_TOPLEVEL); + /** + * PhoshToplevelManager::toplevel-missing: + * @manager: The #PhoshToplevelManager emitting the signal. + * @appinfo: The app info that didn't see a toplevel + * + * Emitted whenever an app from launching app list didn't see a + * toplevel in time. + */ + signals[TOPLEVEL_MISSING] = g_signal_new ("toplevel-missing", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_TYPE_APP_INFO); +} + + +static void +phosh_toplevel_manager_init (PhoshToplevelManager *self) +{ + struct zwlr_foreign_toplevel_manager_v1 *toplevel_manager; + PhoshWayland *wayland = phosh_wayland_get_default (); + + toplevel_manager = phosh_wayland_get_zwlr_foreign_toplevel_manager_v1 (wayland); + + self->launching_apps = g_ptr_array_new_with_free_func ((GDestroyNotify)launching_app_info_free); + self->toplevels = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref); + self->toplevels_pending = g_ptr_array_new (); + + if (!toplevel_manager) { + g_critical ("Missing wlr-foreign-toplevel-management protocol extension"); + return; + } + + zwlr_foreign_toplevel_manager_v1_add_listener (toplevel_manager, + &zwlr_foreign_toplevel_manager_listener, + self); +} + + +PhoshToplevelManager * +phosh_toplevel_manager_new (void) +{ + return g_object_new (PHOSH_TYPE_TOPLEVEL_MANAGER, NULL); +} + +/** + * phosh_toplevel_manager_get_toplevel: + * @self: The toplevel manager + * + * Get the nth toplevel in the list of toplevels + * + * Returns:(transfer none): The toplevel + */ +PhoshToplevel * +phosh_toplevel_manager_get_toplevel (PhoshToplevelManager *self, guint num) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self), NULL); + g_return_val_if_fail (self->toplevels, NULL); + + g_return_val_if_fail (num < self->toplevels->len, NULL); + + return g_ptr_array_index (self->toplevels, num); +} + + +guint +phosh_toplevel_manager_get_num_toplevels (PhoshToplevelManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self), 0); + g_return_val_if_fail (self->toplevels, 0); + + return self->toplevels->len; +} + +/** + * phosh_toplevel_manager_get_parent: + * @self: The toplevel manager + * @toplevel: The toplevel to get the parent for + * + * Gets the parent toplevel of a given toplevel + * + * Returns:(transfer none)(nullable): The toplevel + */ +PhoshToplevel * +phosh_toplevel_manager_get_parent (PhoshToplevelManager *self, PhoshToplevel *toplevel) +{ + struct zwlr_foreign_toplevel_handle_v1 *parent_handle; + + g_return_val_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self), NULL); + g_return_val_if_fail (self->toplevels, NULL); + + parent_handle = phosh_toplevel_get_parent_handle (toplevel); + if (parent_handle == NULL) + return NULL; + + for (int i = 0; i < self->toplevels->len; i++) { + PhoshToplevel *t; + + t = g_ptr_array_index (self->toplevels, i); + if (parent_handle == phosh_toplevel_get_handle (t)) + return t; + } + return NULL; +} + + +void +phosh_toplevel_manager_set_app_tracker (PhoshToplevelManager *self, + PhoshAppTracker *app_tracker) +{ + g_return_if_fail (PHOSH_IS_TOPLEVEL_MANAGER (self)); + g_return_if_fail (PHOSH_IS_APP_TRACKER (app_tracker)); + + if (self->app_tracker) { + g_signal_handlers_disconnect_by_data (self->app_tracker, self); + g_clear_object (&self->app_tracker); + } + + + if (app_tracker) { + self->app_tracker = g_object_ref (app_tracker); + g_signal_connect_swapped (self->app_tracker, + "app-launched", + G_CALLBACK (on_app_launched), + self); + } +} diff --git a/src/toplevel-manager.h b/src/toplevel-manager.h new file mode 100644 index 000000000..5b1650d50 --- /dev/null +++ b/src/toplevel-manager.h @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +#include "app-tracker.h" +#include "toplevel.h" + +#define PHOSH_TYPE_TOPLEVEL_MANAGER (phosh_toplevel_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshToplevelManager, + phosh_toplevel_manager, + PHOSH, + TOPLEVEL_MANAGER, + GObject) + +PhoshToplevelManager *phosh_toplevel_manager_new (void); +PhoshToplevel *phosh_toplevel_manager_get_toplevel (PhoshToplevelManager *self, + guint num); +guint phosh_toplevel_manager_get_num_toplevels (PhoshToplevelManager *self); +PhoshToplevel *phosh_toplevel_manager_get_parent (PhoshToplevelManager *self, + PhoshToplevel *toplevel); +void phosh_toplevel_manager_set_app_tracker (PhoshToplevelManager *self, + PhoshAppTracker *app_tracker); diff --git a/src/toplevel-thumbnail.c b/src/toplevel-thumbnail.c new file mode 100644 index 000000000..5a08d0b32 --- /dev/null +++ b/src/toplevel-thumbnail.c @@ -0,0 +1,265 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Sebastian Krzyszkowiak + */ + +#define G_LOG_DOMAIN "phosh-toplevel-thumbnail" + +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "thumbnail-priv.h" +#include "toplevel-thumbnail.h" +#include "wl-buffer.h" + +#include +#include + +/** + * PhoshToplevelThumbnail: + * + * Represents an image snapshot of PhoshToplevel obtained via phosh-private and wlr-screencopy Wayland protocols. + */ + +enum { + PROP_0, + PROP_HANDLE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshToplevelThumbnail { + PhoshThumbnail parent; + + struct zwlr_screencopy_frame_v1 *handle; + PhoshWlBuffer *buffer; +}; + +G_DEFINE_TYPE (PhoshToplevelThumbnail, phosh_toplevel_thumbnail, PHOSH_TYPE_THUMBNAIL); + + +static void +screencopy_handle_buffer (void *data, + struct zwlr_screencopy_frame_v1 *zwlr_screencopy_frame_v1, + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t stride) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (data); + + g_debug ("%s: width %d height %d stride %d", __func__, width, height, stride); + + if (stride * height == 0) { + g_warning ("Got %s with no size!", __func__); + return; + } + + self->buffer = phosh_wl_buffer_new (format, width, height, stride); + zwlr_screencopy_frame_v1_copy (zwlr_screencopy_frame_v1, self->buffer->wl_buffer); +} + + +static void +screencopy_handle_flags (void *data, + struct zwlr_screencopy_frame_v1 *zwlr_screencopy_frame_v1, + uint32_t flags) +{ + /* Nothing to do */ +} + + +static void +screencopy_handle_ready (void *data, + struct zwlr_screencopy_frame_v1 *zwlr_screencopy_frame_v1, + uint32_t tv_sec_hi, + uint32_t tv_sec_lo, + uint32_t tv_nsec) +{ + phosh_thumbnail_set_ready (PHOSH_THUMBNAIL (data), TRUE); +} + +static void +screencopy_handle_failed (void *data, + struct zwlr_screencopy_frame_v1 *zwlr_screencopy_frame_v1) +{ + g_warning ("screencopy failed! %p", data); +} + +static void +screencopy_handle_damage (void *data, + struct zwlr_screencopy_frame_v1 *zwlr_screencopy_frame_v1, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height) +{ +} + +static const struct zwlr_screencopy_frame_v1_listener zwlr_screencopy_frame_listener = { + screencopy_handle_buffer, + screencopy_handle_flags, + screencopy_handle_ready, + screencopy_handle_failed, + screencopy_handle_damage +}; + + +static void * +phosh_toplevel_thumbnail_get_image (PhoshThumbnail *thumbnail) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (thumbnail); + + g_return_val_if_fail (PHOSH_IS_TOPLEVEL_THUMBNAIL (self), NULL); + return self->buffer->data; +} + +static void +phosh_toplevel_thumbnail_get_size (PhoshThumbnail *thumbnail, + guint *width, + guint *height, + guint *stride) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (thumbnail); + if (width) + *width = self->buffer->width; + if (height) + *height = self->buffer->height; + if (stride) + *stride = self->buffer->stride; +} + + +static void +phosh_toplevel_thumbnail_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (object); + + switch (property_id) { + case PROP_HANDLE: + self->handle = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_toplevel_thumbnail_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (object); + + switch (property_id) { + case PROP_HANDLE: + g_value_set_pointer (value, self->handle); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_toplevel_thumbnail_constructed (GObject *object) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (object); + zwlr_screencopy_frame_v1_add_listener (self->handle, &zwlr_screencopy_frame_listener, self); + + G_OBJECT_CLASS (phosh_toplevel_thumbnail_parent_class)->constructed (object); +} + + +static void +phosh_toplevel_thumbnail_dispose (GObject *object) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (object); + + g_clear_pointer (&self->handle, zwlr_screencopy_frame_v1_destroy); + + G_OBJECT_CLASS (phosh_toplevel_thumbnail_parent_class)->dispose (object); +} + + +static void +phosh_toplevel_thumbnail_finalize (GObject *object) +{ + PhoshToplevelThumbnail *self = PHOSH_TOPLEVEL_THUMBNAIL (object); + + g_clear_pointer (&self->buffer, phosh_wl_buffer_destroy); + + G_OBJECT_CLASS (phosh_toplevel_thumbnail_parent_class)->finalize (object); +} + + +static void +phosh_toplevel_thumbnail_class_init (PhoshToplevelThumbnailClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshThumbnailClass *thumbnail_class = PHOSH_THUMBNAIL_CLASS (klass); + + object_class->set_property = phosh_toplevel_thumbnail_set_property; + object_class->get_property = phosh_toplevel_thumbnail_get_property; + object_class->constructed = phosh_toplevel_thumbnail_constructed; + object_class->dispose = phosh_toplevel_thumbnail_dispose; + object_class->finalize = phosh_toplevel_thumbnail_finalize; + + thumbnail_class->get_image = phosh_toplevel_thumbnail_get_image; + thumbnail_class->get_size = phosh_toplevel_thumbnail_get_size; + + /** + * PhoshToplevelThumbnail:handle: + * + * The zwlr_screencopy_frame_v1 object associated with this + * thumbnail + */ + props[PROP_HANDLE] = + g_param_spec_pointer ("handle", "", "", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_toplevel_thumbnail_init (PhoshToplevelThumbnail *self) +{ +} + + +static PhoshToplevelThumbnail * +phosh_toplevel_thumbnail_new_from_handle (struct zwlr_screencopy_frame_v1 *handle) +{ + return g_object_new (PHOSH_TYPE_TOPLEVEL_THUMBNAIL, "handle", handle, NULL); +} + +PhoshToplevelThumbnail * +phosh_toplevel_thumbnail_new_from_toplevel (PhoshToplevel *toplevel, + guint32 max_width, + guint32 max_height) +{ + struct zwlr_foreign_toplevel_handle_v1 *handle = phosh_toplevel_get_handle (toplevel); + struct phosh_private *phosh = phosh_wayland_get_phosh_private (phosh_wayland_get_default ()); + struct zwlr_screencopy_frame_v1 *frame; + + if (!phosh || phosh_private_get_version (phosh) < PHOSH_PRIVATE_GET_THUMBNAIL_SINCE_VERSION) + return NULL; + + g_debug ("Requesting a %dx%d thumbnail for toplevel %p [%s]", max_width, max_height, + toplevel, phosh_toplevel_get_title (toplevel)); + + frame = phosh_private_get_thumbnail (phosh, handle, max_width, max_height); + + return phosh_toplevel_thumbnail_new_from_handle (frame); +} diff --git a/src/toplevel-thumbnail.h b/src/toplevel-thumbnail.h new file mode 100644 index 000000000..09f49ef14 --- /dev/null +++ b/src/toplevel-thumbnail.h @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "wlr-screencopy-unstable-v1-client-protocol.h" +#include "thumbnail.h" +#include "toplevel.h" + +#define PHOSH_TYPE_TOPLEVEL_THUMBNAIL (phosh_toplevel_thumbnail_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshToplevelThumbnail, + phosh_toplevel_thumbnail, + PHOSH, + TOPLEVEL_THUMBNAIL, + PhoshThumbnail) + +PhoshToplevelThumbnail *phosh_toplevel_thumbnail_new_from_toplevel (PhoshToplevel *toplevel, + guint32 max_width, + guint32 max_height); diff --git a/src/toplevel.c b/src/toplevel.c new file mode 100644 index 000000000..baf929e17 --- /dev/null +++ b/src/toplevel.c @@ -0,0 +1,469 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Sebastian Krzyszkowiak + */ + +#define G_LOG_DOMAIN "phosh-toplevel" + +#include "toplevel.h" +#include "phosh-wayland.h" +#include "shell-priv.h" +#include "util.h" + +#include + +/** + * PhoshToplevel: + * + * Represents a single toplevel surface. + */ + +enum { + PHOSH_TOPLEVEL_PROP_0, + PHOSH_TOPLEVEL_PROP_HANDLE, + PHOSH_TOPLEVEL_PROP_CONFIGURED, + PHOSH_TOPLEVEL_PROP_TITLE, + PHOSH_TOPLEVEL_PROP_APP_ID, + PHOSH_TOPLEVEL_PROP_ACTIVATED, + PHOSH_TOPLEVEL_PROP_MAXIMIZED, + PHOSH_TOPLEVEL_PROP_FULLSCREEN, + PHOSH_TOPLEVEL_PROP_LAST_PROP, +}; +static GParamSpec *props[PHOSH_TOPLEVEL_PROP_LAST_PROP]; + +enum { + SIGNAL_CLOSED, + N_SIGNALS +}; +static guint signals[N_SIGNALS] = { 0 }; + +struct _PhoshToplevel { + GObject parent; + struct zwlr_foreign_toplevel_handle_v1 *handle; + struct zwlr_foreign_toplevel_handle_v1 *parent_handle; + gboolean configured, activated, maximized, fullscreen; + char *title; + char *app_id; +}; + +G_DEFINE_TYPE (PhoshToplevel, phosh_toplevel, G_TYPE_OBJECT); + + +static void +handle_zwlr_foreign_toplevel_handle_title( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + const char* title) +{ + PhoshToplevel *self = data; + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + g_free (self->title); + self->title = g_strdup (title); + g_debug ("%p: Got title %s", zwlr_foreign_toplevel_handle_v1, title); + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_TITLE]); +} + + +static void +handle_zwlr_foreign_toplevel_handle_app_id( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + const char* app_id) +{ + PhoshToplevel *self = data; + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + g_free (self->app_id); + self->app_id = g_strdup (app_id); + g_debug ("%p: Got app_id %s", zwlr_foreign_toplevel_handle_v1, app_id); + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_APP_ID]); +} + + +static void +handle_zwlr_foreign_toplevel_handle_output_enter( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_output *output) +{ +} + + +static void +handle_zwlr_foreign_toplevel_handle_output_leave( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_output *output) +{ +} + + +static void +handle_zwlr_foreign_toplevel_handle_state( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct wl_array *state) +{ + PhoshToplevel *self = data; + enum zwlr_foreign_toplevel_handle_v1_state *value; + gboolean activated = FALSE, maximized = FALSE, fullscreen = FALSE; + + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + wl_array_for_each (value, state) { + g_debug("toplevel_handle %p: has state %d", self, *value); + + if (*value == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED) { + activated = TRUE; + } + if (*value == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED) { + maximized = TRUE; + } + if (*value == ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN) { + fullscreen = TRUE; + } + } + + if (self->activated != activated) { + self->activated = activated; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_ACTIVATED]); + } + + if (self->maximized != maximized) { + self->maximized = maximized; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_MAXIMIZED]); + } + + if (self->fullscreen != fullscreen) { + self->fullscreen = fullscreen; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_FULLSCREEN]); + } +} + + +static void +handle_zwlr_foreign_toplevel_handle_done( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + PhoshToplevel *self = data; + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + self->configured = TRUE; + g_object_notify_by_pspec (G_OBJECT (self), props[PHOSH_TOPLEVEL_PROP_CONFIGURED]); +} + + +static void +handle_zwlr_foreign_toplevel_handle_closed( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1) +{ + PhoshToplevel *self = data; + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + g_signal_emit (self, signals[SIGNAL_CLOSED], 0); +} + + +static void +handle_zwlr_foreign_toplevel_handle_parent ( + void *data, + struct zwlr_foreign_toplevel_handle_v1 *zwlr_foreign_toplevel_handle_v1, + struct zwlr_foreign_toplevel_handle_v1 *parent) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (data); + + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + g_debug ("Got parent handle %p", parent); + self->parent_handle = parent; +} + + +static const struct zwlr_foreign_toplevel_handle_v1_listener zwlr_foreign_toplevel_handle_listener = { + handle_zwlr_foreign_toplevel_handle_title, + handle_zwlr_foreign_toplevel_handle_app_id, + handle_zwlr_foreign_toplevel_handle_output_enter, + handle_zwlr_foreign_toplevel_handle_output_leave, + handle_zwlr_foreign_toplevel_handle_state, + handle_zwlr_foreign_toplevel_handle_done, + handle_zwlr_foreign_toplevel_handle_closed, + handle_zwlr_foreign_toplevel_handle_parent, +}; + + +static void +phosh_toplevel_dispose (GObject *object) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (object); + + g_clear_pointer (&self->handle, zwlr_foreign_toplevel_handle_v1_destroy); + + G_OBJECT_CLASS (phosh_toplevel_parent_class)->dispose (object); +} + + +static void +phosh_toplevel_finalize (GObject *object) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (object); + + g_clear_pointer (&self->app_id, g_free); + g_clear_pointer (&self->title, g_free); + + G_OBJECT_CLASS (phosh_toplevel_parent_class)->finalize (object); +} + + +static void +phosh_toplevel_constructed (GObject *object) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (object); + + zwlr_foreign_toplevel_handle_v1_add_listener (self->handle, &zwlr_foreign_toplevel_handle_listener, self); + zwlr_foreign_toplevel_handle_v1_set_user_data (self->handle, self); + + G_OBJECT_CLASS (phosh_toplevel_parent_class)->constructed (object); +} + + +static void +phosh_toplevel_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (object); + + switch (property_id) { + case PHOSH_TOPLEVEL_PROP_HANDLE: + self->handle = g_value_get_pointer (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_toplevel_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshToplevel *self = PHOSH_TOPLEVEL (object); + + switch (property_id) { + case PHOSH_TOPLEVEL_PROP_HANDLE: + g_value_set_pointer (value, self->handle); + break; + case PHOSH_TOPLEVEL_PROP_CONFIGURED: + g_value_set_boolean (value, self->configured); + break; + case PHOSH_TOPLEVEL_PROP_ACTIVATED: + g_value_set_boolean (value, self->activated); + break; + case PHOSH_TOPLEVEL_PROP_MAXIMIZED: + g_value_set_boolean (value, self->maximized); + break; + case PHOSH_TOPLEVEL_PROP_FULLSCREEN: + g_value_set_boolean (value, self->fullscreen); + break; + case PHOSH_TOPLEVEL_PROP_TITLE: + g_value_set_string (value, self->title); + break; + case PHOSH_TOPLEVEL_PROP_APP_ID: + g_value_set_string (value, self->app_id); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +phosh_toplevel_class_init (PhoshToplevelClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = phosh_toplevel_set_property; + object_class->get_property = phosh_toplevel_get_property; + object_class->constructed = phosh_toplevel_constructed; + object_class->dispose = phosh_toplevel_dispose; + object_class->finalize = phosh_toplevel_finalize; + + props[PHOSH_TOPLEVEL_PROP_HANDLE] = + g_param_spec_pointer ("handle", + "handle", + "The zwlr_foreign_toplevel_handle_v1 object associated with this toplevel", + G_PARAM_READWRITE | + G_PARAM_CONSTRUCT_ONLY | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_CONFIGURED] = + g_param_spec_boolean ("configured", + "configured", + "Whether the toplevel has been already filled with all initial data", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_ACTIVATED] = + g_param_spec_boolean ("activated", + "activated", + "Whether the toplevel is currently focused", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_MAXIMIZED] = + g_param_spec_boolean ("maximized", + "maximized", + "Whether the toplevel is currently maximized", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_FULLSCREEN] = + g_param_spec_boolean ("fullscreen", + "fullscreen", + "Whether the toplevel is currently presented fullscreen", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_TITLE] = + g_param_spec_string ("title", + "title", + "The window's title", + "", + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + props[PHOSH_TOPLEVEL_PROP_APP_ID] = + g_param_spec_string ("app-id", + "app-id", + "The application ID", + "", + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PHOSH_TOPLEVEL_PROP_LAST_PROP, props); + + /** + * PhoshToplevel::closed: + * @toplevel: The #PhoshToplevel emitting the signal. + * + * Emitted when a toplevel has been closed. + */ + signals[SIGNAL_CLOSED] = g_signal_new ( + "closed", + G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 0); +} + + +static void +phosh_toplevel_init (PhoshToplevel *self) +{ +} + + +PhoshToplevel * +phosh_toplevel_new_from_handle (struct zwlr_foreign_toplevel_handle_v1 *handle) +{ + return g_object_new (PHOSH_TYPE_TOPLEVEL, "handle", handle, NULL); +} + + +const char * +phosh_toplevel_get_title (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), NULL); + return self->title; +} + + +const char * +phosh_toplevel_get_app_id (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), NULL); + return self->app_id; +} + + +struct zwlr_foreign_toplevel_handle_v1 * +phosh_toplevel_get_handle (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), NULL); + return self->handle; +} + + +struct zwlr_foreign_toplevel_handle_v1 * +phosh_toplevel_get_parent_handle (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), NULL); + return self->parent_handle; +} + + +gboolean +phosh_toplevel_is_configured (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), FALSE); + return self->configured; +} + + +gboolean +phosh_toplevel_is_activated (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), FALSE); + return self->activated; +} + + +gboolean +phosh_toplevel_is_maximized (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), FALSE); + return self->maximized; +} + + +gboolean +phosh_toplevel_is_fullscreen (PhoshToplevel *self) +{ + g_return_val_if_fail (PHOSH_IS_TOPLEVEL (self), FALSE); + return self->fullscreen; +} + + +void +phosh_toplevel_activate (PhoshToplevel *self, struct wl_seat *seat) +{ + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + zwlr_foreign_toplevel_handle_v1_activate (self->handle, seat); +} + + +void +phosh_toplevel_close (PhoshToplevel *self) +{ + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + zwlr_foreign_toplevel_handle_v1_close (self->handle); +} + + +void +phosh_toplevel_fullscreen (PhoshToplevel *self, gboolean fullscreen) +{ + g_return_if_fail (PHOSH_IS_TOPLEVEL (self)); + + if (fullscreen) + zwlr_foreign_toplevel_handle_v1_set_fullscreen (self->handle, NULL); + else + zwlr_foreign_toplevel_handle_v1_unset_fullscreen (self->handle); +} diff --git a/src/toplevel.h b/src/toplevel.h new file mode 100644 index 000000000..4a8d16424 --- /dev/null +++ b/src/toplevel.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" +#include + +#define PHOSH_TYPE_TOPLEVEL (phosh_toplevel_get_type()) + +G_DECLARE_FINAL_TYPE (PhoshToplevel, + phosh_toplevel, + PHOSH, + TOPLEVEL, + GObject) + +PhoshToplevel *phosh_toplevel_new_from_handle (struct zwlr_foreign_toplevel_handle_v1 *handle); +const char *phosh_toplevel_get_title (PhoshToplevel *self); +const char *phosh_toplevel_get_app_id (PhoshToplevel *self); +struct zwlr_foreign_toplevel_handle_v1 *phosh_toplevel_get_handle (PhoshToplevel *self); +struct zwlr_foreign_toplevel_handle_v1 *phosh_toplevel_get_parent_handle (PhoshToplevel *self); +gboolean phosh_toplevel_is_configured (PhoshToplevel *self); +gboolean phosh_toplevel_is_activated (PhoshToplevel *self); +gboolean phosh_toplevel_is_maximized (PhoshToplevel *self); +gboolean phosh_toplevel_is_fullscreen (PhoshToplevel *self); +void phosh_toplevel_activate (PhoshToplevel *self, struct wl_seat *seat); +void phosh_toplevel_close (PhoshToplevel *self); +void phosh_toplevel_fullscreen (PhoshToplevel *self, gboolean fullscreen); diff --git a/src/torch-info.c b/src/torch-info.c new file mode 100644 index 000000000..e8e99f0b4 --- /dev/null +++ b/src/torch-info.c @@ -0,0 +1,233 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-torch-info" + +#include "phosh-config.h" + +#include "shell-priv.h" +#include "torch-info.h" +#include "torch-manager.h" + +/** + * PhoshTorchInfo: + * + * A widget to display the torch status + * + * #PhoshTorchInfo displays the current torch status based on information + * from #PhoshTorchManager. To figure out if the widget should be shown + * the #PhoshTorchInfo:enabled property can be useful. + */ + +enum { + PROP_0, + PROP_ENABLED, + PROP_PRESENT, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + + +struct _PhoshTorchInfo { + PhoshStatusIcon parent; + + gboolean enabled; + gboolean present; + PhoshTorchManager *torch; +}; +G_DEFINE_TYPE (PhoshTorchInfo, phosh_torch_info, PHOSH_TYPE_STATUS_ICON); + + +static void +phosh_torch_info_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTorchInfo *self = PHOSH_TORCH_INFO (object); + + switch (property_id) { + case PROP_ENABLED: + g_value_set_boolean (value, self->enabled); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +update_info (PhoshTorchInfo *self, GParamSpec *pspec, PhoshTorchManager *torch) +{ + g_return_if_fail (PHOSH_IS_TORCH_INFO (self)); + + if (phosh_torch_manager_get_enabled (self->torch)) { + g_autofree char *str = NULL; + double frac = phosh_torch_manager_get_scaled_brightness (self->torch); + + /* If the flashlight can't scale brightness on means 100% */ + if (!phosh_torch_manager_get_can_scale (self->torch)) + frac = 1.0; + + str = g_strdup_printf ("%d%%", (int)(frac * 100.0)); + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), str); + } else { + phosh_status_icon_set_info (PHOSH_STATUS_ICON (self), _("Torch")); + } +} + + +static void +on_torch_enabled (PhoshTorchInfo *self, GParamSpec *pspec, PhoshTorchManager *torch) +{ + gboolean enabled; + + g_return_if_fail (PHOSH_IS_TORCH_INFO (self)); + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (torch)); + + enabled = phosh_torch_manager_get_enabled (torch); + if (self->enabled == enabled) + return; + + self->enabled = enabled; + g_debug ("Updating torch enabled: %d", enabled); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); +} + + +static void +on_torch_present (PhoshTorchInfo *self, GParamSpec *pspec, PhoshTorchManager *torch) +{ + gboolean present; + + g_return_if_fail (PHOSH_IS_TORCH_INFO (self)); + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (torch)); + + present = phosh_torch_manager_get_present (torch); + if (self->present == present) + return; + + self->present = present; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); +} + + +static void +phosh_torch_info_idle_init (PhoshStatusIcon *icon) +{ + PhoshTorchInfo *self = PHOSH_TORCH_INFO (icon); + + g_object_bind_property (self->torch, "icon-name", self, "icon-name", + G_BINDING_SYNC_CREATE); + + g_signal_connect_swapped (self->torch, + "notify::brightness", + G_CALLBACK (update_info), + self); + update_info (self, NULL, self->torch); + + /* We don't use a binding for self->enabled so we can keep + the property r/o */ + g_signal_connect_swapped (self->torch, + "notify::enabled", + G_CALLBACK (on_torch_enabled), + self); + on_torch_enabled (self, NULL, self->torch); + + g_signal_connect_swapped (self->torch, + "notify::present", + G_CALLBACK (on_torch_present), + self); + on_torch_present (self, NULL, self->torch); +} + + +static void +phosh_torch_info_constructed (GObject *object) +{ + PhoshTorchInfo *self = PHOSH_TORCH_INFO (object); + PhoshShell *shell; + + G_OBJECT_CLASS (phosh_torch_info_parent_class)->constructed (object); + + shell = phosh_shell_get_default (); + self->torch = g_object_ref (phosh_shell_get_torch_manager (shell)); + + if (self->torch == NULL) { + g_warning ("Failed to get torch manager"); + return; + } +} + + +static void +phosh_torch_info_dispose (GObject *object) +{ + PhoshTorchInfo *self = PHOSH_TORCH_INFO (object); + + if (self->torch) { + g_signal_handlers_disconnect_by_data (self->torch, self); + g_clear_object (&self->torch); + } + + G_OBJECT_CLASS (phosh_torch_info_parent_class)->dispose (object); +} + + +static void +phosh_torch_info_class_init (PhoshTorchInfoClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshStatusIconClass *status_icon_class = PHOSH_STATUS_ICON_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + + object_class->constructed = phosh_torch_info_constructed; + object_class->dispose = phosh_torch_info_dispose; + object_class->get_property = phosh_torch_info_get_property; + + status_icon_class->idle_init = phosh_torch_info_idle_init; + + gtk_widget_class_set_css_name (widget_class, "phosh-torch-info"); + + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", + "enabled", + "Whether the torch is enabled", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + props[PROP_PRESENT] = + g_param_spec_boolean ("present", + "Present", + "Whether torch hardware is present", + FALSE, + G_PARAM_READABLE | + G_PARAM_STATIC_STRINGS | + G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_torch_info_init (PhoshTorchInfo *self) +{ +} + + +GtkWidget * +phosh_torch_info_new (void) +{ + return g_object_new (PHOSH_TYPE_TORCH_INFO, NULL); +} diff --git a/src/torch-info.h b/src/torch-info.h new file mode 100644 index 000000000..eddbe4467 --- /dev/null +++ b/src/torch-info.h @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include +#include "status-icon.h" + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TORCH_INFO (phosh_torch_info_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTorchInfo, phosh_torch_info, PHOSH, TORCH_INFO, PhoshStatusIcon) + +GtkWidget * phosh_torch_info_new (void); + +G_END_DECLS diff --git a/src/torch-manager.c b/src/torch-manager.c new file mode 100644 index 000000000..33b97cafd --- /dev/null +++ b/src/torch-manager.c @@ -0,0 +1,445 @@ +/* + * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-torch-manager" + +#include "phosh-config.h" + +#include + +#include "torch-manager.h" +#include "udev-manager.h" +#include "dbus/login1-session-dbus.h" + +#include + +#define TORCH_SUBSYSTEM "leds" + +#define TORCH_DISABLED_ICON "torch-disabled-symbolic" +#define TORCH_ENABLED_ICON "torch-enabled-symbolic" + +/** + * PhoshTorchManager: + * + * Interacts with torch via sysfs and logind + * + * #PhoshTorchManager tracks the torch status and + * allows to set the brightness. + */ + +enum { + PROP_0, + PROP_ICON_NAME, + PROP_ENABLED, + PROP_PRESENT, + PROP_CAN_SCALE, + PROP_BRIGHTNESS, + PROP_LAST_PROP +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _PhoshTorchManager { + PhoshManager parent; + + /* Whether we found a torch device */ + gboolean present; + const char *icon_name; + gboolean can_scale; + int brightness; + int min_brightness; + int max_brightness; + int last_brightness; + + GUdevDevice *udev_device; + + PhoshDBusLoginSession *session_proxy; + GCancellable *cancel; +}; +G_DEFINE_TYPE (PhoshTorchManager, phosh_torch_manager, PHOSH_TYPE_MANAGER); + + +static void +apply_brightness (PhoshTorchManager *self) +{ + const char *icon_name; + + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (self)); + g_return_if_fail (G_UDEV_IS_DEVICE (self->udev_device)); + + g_object_freeze_notify (G_OBJECT (self)); + + self->brightness = g_udev_device_get_sysfs_attr_as_int_uncached (self->udev_device, + "brightness"); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BRIGHTNESS]); + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]); + + icon_name = self->brightness ? TORCH_ENABLED_ICON : TORCH_DISABLED_ICON; + if (icon_name != self->icon_name) { + self->icon_name = icon_name; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ICON_NAME]); + } + + g_object_thaw_notify (G_OBJECT (self)); +} + + +static void +on_brightness_set (GObject *source_object, GAsyncResult *res, gpointer user_data) +{ + PhoshDBusLoginSession *proxy = PHOSH_DBUS_LOGIN_SESSION (source_object); + PhoshTorchManager *self = PHOSH_TORCH_MANAGER (user_data); + g_autoptr (GError) err = NULL; + + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_LOGIN_SESSION (proxy)); + + if (!phosh_dbus_login_session_call_set_brightness_finish (proxy, res, &err)) { + g_warning ("Failed to set torch brightness: %s", err->message); + return; + } + + apply_brightness (self); +} + + +static void +set_brightness (PhoshTorchManager *self, int brightness) +{ + g_return_if_fail (G_UDEV_IS_DEVICE (self->udev_device)); + + if (self->brightness == brightness) + return; + + g_debug ("Setting brightness to %d", brightness); + + phosh_dbus_login_session_call_set_brightness (self->session_proxy, + TORCH_SUBSYSTEM, + g_udev_device_get_name (self->udev_device), + (guint) brightness, + NULL, + on_brightness_set, + self); +} + +static void +phosh_torch_manager_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + PhoshTorchManager *self = PHOSH_TORCH_MANAGER (object); + + switch (property_id) { + case PROP_ICON_NAME: + g_value_set_string (value, self->icon_name); + break; + case PROP_PRESENT: + g_value_set_boolean (value, self->present); + break; + case PROP_ENABLED: + g_value_set_boolean (value, !!self->brightness); + break; + case PROP_CAN_SCALE: + g_value_set_boolean (value, self->can_scale); + break; + case PROP_BRIGHTNESS: + g_value_set_int (value, self->brightness); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +get_first_torch_device (gpointer data, gpointer manager) +{ + PhoshTorchManager *self = PHOSH_TORCH_MANAGER (manager); + GUdevDevice *device = G_UDEV_DEVICE (data); + + /* Keep only the first torch device found */ + if (!self->udev_device) + self->udev_device = g_object_ref (device); +} + + +static gboolean +find_torch_device (PhoshTorchManager *self, PhoshUdevManager *udev_manager) +{ + g_autolist (GUdevDevice) device_list = NULL; + g_autoptr (GError) err = NULL; + + g_clear_object (&self->udev_device); + + device_list = phosh_udev_manager_find_torches (udev_manager, &err); + if (!device_list) { + g_debug ("Failed to find a torch device: %s", err->message); + return FALSE; + } + + g_list_foreach (device_list, get_first_torch_device, self); + if (!self->udev_device) { + g_warning ("Failed to find a usable torch device"); + return FALSE; + } + + self->min_brightness = g_udev_device_get_property_as_int (self->udev_device, + "GM_TORCH_MIN_BRIGHTNESS"); + self->max_brightness = g_udev_device_get_sysfs_attr_as_int (self->udev_device, + "max_brightness"); + + /* setting min_brightness to 5% to have a proper lower limit */ + if (!self->min_brightness) + self->min_brightness = MAX (self->max_brightness * 0.05, 1); + + g_debug ("Found torch device '%s' with min brightness %d and max brightness %d", + g_udev_device_get_name (self->udev_device),self->min_brightness,self->max_brightness); + + self->can_scale = self->min_brightness < self->max_brightness && self->max_brightness > 1; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CAN_SCALE]); + + return TRUE; +} + + +static void +phosh_torch_manager_idle_init (PhoshManager *manager) +{ + PhoshTorchManager *self = PHOSH_TORCH_MANAGER (manager); + PhoshUdevManager *udev_manager = phosh_udev_manager_get_default (); + + self->session_proxy = phosh_udev_manager_get_session_proxy (udev_manager); + self->present = find_torch_device (self, udev_manager); + + if (self->present) { + g_object_freeze_notify (G_OBJECT (self)); + + apply_brightness (self); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRESENT]); + g_object_thaw_notify (G_OBJECT (self)); + } +} + + +static void +phosh_torch_manager_dispose (GObject *object) +{ + PhoshTorchManager *self = PHOSH_TORCH_MANAGER (object); + + g_cancellable_cancel (self->cancel); + g_clear_object (&self->cancel); + + g_clear_object (&self->session_proxy); + + g_clear_object (&self->udev_device); + + G_OBJECT_CLASS (phosh_torch_manager_parent_class)->dispose (object); +} + + +static void +phosh_torch_manager_class_init (PhoshTorchManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + PhoshManagerClass *manager_class = PHOSH_MANAGER_CLASS (klass); + + object_class->get_property = phosh_torch_manager_get_property; + object_class->dispose = phosh_torch_manager_dispose; + + manager_class->idle_init = phosh_torch_manager_idle_init; + + /** + * PhoshTorchManager:icon-name: + * + * The icon-name of torch + */ + props[PROP_ICON_NAME] = + g_param_spec_string ("icon-name", "", "", + "torch-disabled-symbolic", + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + /** + * PhoshTorchManager:enabled: + * + * Whether torch is enabled + */ + props[PROP_ENABLED] = + g_param_spec_boolean ("enabled", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * PhoshTorchManager:present: + * + * Whether a torch is present + */ + props[PROP_PRESENT] = + g_param_spec_boolean ("present", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * PhoshTorchManager:can-scale: + * + * Whether the brightness can be scaled + */ + props[PROP_CAN_SCALE] = + g_param_spec_boolean ("can-scale", "", "", + FALSE, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + /** + * PhoshTorchManager:brightness: + * + * The brightness of torch + */ + props[PROP_BRIGHTNESS] = + g_param_spec_int ("brightness", "", "", + 0, G_MAXINT, 0, + G_PARAM_READABLE | + G_PARAM_EXPLICIT_NOTIFY | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); +} + + +static void +phosh_torch_manager_init (PhoshTorchManager *self) +{ + self->icon_name = TORCH_DISABLED_ICON; + self->max_brightness = 1; +} + + +PhoshTorchManager * +phosh_torch_manager_new (void) +{ + return PHOSH_TORCH_MANAGER (g_object_new (PHOSH_TYPE_TORCH_MANAGER, NULL)); +} + + +const char * +phosh_torch_manager_get_icon_name (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), NULL); + + return self->icon_name; +} + + +gboolean +phosh_torch_manager_get_enabled (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), FALSE); + + return !!self->brightness; +} + + +gboolean +phosh_torch_manager_get_present (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), FALSE); + + return self->present; +} + +gboolean +phosh_torch_manager_get_can_scale (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), FALSE); + + return self->can_scale; +} + +int +phosh_torch_manager_get_brightness (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), 0); + + return self->brightness; +} + +/** + * phosh_torch_manager_get_scaled_brightness: + * @self: The #PhoshTorchManager + * + * Gets the current brightness as fraction between 0.01 (minimum_brightness) and 1.0 + * (maximum brightness). + */ +double +phosh_torch_manager_get_scaled_brightness (PhoshTorchManager *self) +{ + double val; + + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), 0); + + val = (double)(self->brightness - self->min_brightness) / + (double)(self->max_brightness - self->min_brightness); + + /* Since the torch should never be fully off when enabled, always return a non-0 value */ + return MAX (0.01, val); +} + +/** + * phosh_torch_manager_set_scaled_brightness: + * @self: The #PhoshTorchManager + * @frac: The brightness as fraction + * + * Sets the current brightness as fraction between 0 (minimum_brightness) and 1 (maximum brightness) + */ +void +phosh_torch_manager_set_scaled_brightness (PhoshTorchManager *self, double frac) +{ + int brightness; + + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (self)); + g_return_if_fail (frac >= 0.0 && frac <= 1.0); + + brightness = round (self->min_brightness + frac * (self->max_brightness - self->min_brightness)); + if (brightness > self->max_brightness) + brightness = self->max_brightness; + + set_brightness (self, brightness); +} + + +int +phosh_torch_manager_get_max_brightness (PhoshTorchManager *self) +{ + g_return_val_if_fail (PHOSH_IS_TORCH_MANAGER (self), 0); + + return self->max_brightness; +} + + +void +phosh_torch_manager_toggle (PhoshTorchManager *self) +{ + g_return_if_fail (PHOSH_IS_TORCH_MANAGER (self)); + g_return_if_fail (PHOSH_DBUS_IS_LOGIN_SESSION (self->session_proxy)); + + if (self->brightness) { + g_debug ("Disabling torch"); + self->last_brightness = self->brightness; + set_brightness (self, 0); + } else { + if (self->last_brightness == 0) { + self->last_brightness = self->max_brightness; + g_debug ("Last brightness: %d", self->last_brightness); + } + g_debug ("Setting torch brightness to %d", self->last_brightness); + set_brightness (self, self->last_brightness); + } +} diff --git a/src/torch-manager.h b/src/torch-manager.h new file mode 100644 index 000000000..7e3d1cb2e --- /dev/null +++ b/src/torch-manager.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2020 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "manager.h" + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_TORCH_MANAGER (phosh_torch_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshTorchManager, phosh_torch_manager, PHOSH, TORCH_MANAGER, PhoshManager) + +PhoshTorchManager *phosh_torch_manager_new (void); +const char *phosh_torch_manager_get_icon_name (PhoshTorchManager *self); +gboolean phosh_torch_manager_get_enabled (PhoshTorchManager *self); +gboolean phosh_torch_manager_get_present (PhoshTorchManager *self); +void phosh_torch_manager_toggle (PhoshTorchManager *self); +gboolean phosh_torch_manager_get_can_scale (PhoshTorchManager *self); +int phosh_torch_manager_get_brightness (PhoshTorchManager *self); +double phosh_torch_manager_get_scaled_brightness (PhoshTorchManager *self); +void phosh_torch_manager_set_scaled_brightness (PhoshTorchManager *self, double frac); +int phosh_torch_manager_get_max_brightness (PhoshTorchManager *self); + +G_END_DECLS diff --git a/src/udev-manager.c b/src/udev-manager.c new file mode 100644 index 000000000..07c8d9585 --- /dev/null +++ b/src/udev-manager.c @@ -0,0 +1,271 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Guido Günther + */ + +#define G_LOG_DOMAIN "phosh-udev-manager" + +#include "phosh-config.h" + +#include "dbus/login1-session-dbus.h" +#include "monitor/monitor.h" +#include "shell-priv.h" +#include "udev-manager.h" + +#define BUS_NAME "org.freedesktop.login1" +#define OBJECT_PATH "/org/freedesktop/login1/session/auto" + +#define BACKLIGHT_SUBSYSTEM "backlight" +#define LEDS_SUBSYSTEM "leds" + +/** + * PhoshUdevManager: + * + * Manage a udev client for the subsystems we're interested in + */ + +enum { + BACKLIGHT_CHANGED, + N_SIGNALS +}; +static guint signals[N_SIGNALS]; + +struct _PhoshUdevManager { + GObject parent; + + GUdevClient *udev_client; + + PhoshDBusLoginSession *session_proxy; +}; + +G_DEFINE_TYPE (PhoshUdevManager, phosh_udev_manager, G_TYPE_OBJECT) + + +static GUdevDevice * +phosh_backlight_sysfs_udev_get_type (GList *devices, const char *type) +{ + for (GList *d = devices; d != NULL; d = d->next) { + GUdevDevice *device = d->data; + const char *t; + + t = g_udev_device_get_sysfs_attr (device, "type"); + if (g_strcmp0 (t, type) == 0) + return g_object_ref (device); + } + return NULL; +} + + +static GUdevDevice * +phosh_backlight_sysfs_udev_get_raw (GList *devices, + const char *connector_name) +{ + for (GList *d = devices; d != NULL; d = d->next) { + g_autoptr (GUdevDevice) parent = NULL; + GUdevDevice *device = d->data; + const char *attr = g_udev_device_get_sysfs_attr (device, "type"); + const char *prop; + + if (g_strcmp0 (attr, "raw") != 0) + continue; + + parent = g_udev_device_get_parent (device); + if (!parent) + continue; + + prop = g_udev_device_get_subsystem (parent); + if (g_strcmp0 (prop, "drm") != 0) + continue; + + /* The drm-connector name `card[n]-[connector-name]` */ + prop = g_udev_device_get_name (parent); + if (!prop || !g_str_has_suffix (prop, connector_name)) + continue; + + attr = g_udev_device_get_sysfs_attr (parent, "enabled"); + if (g_strcmp0 (attr, "enabled") != 0) + continue; + + return g_object_ref (device); + } + + return NULL; +} + + +static void +on_uevent (PhoshUdevManager *self, const char *action, GUdevDevice *device) +{ + if (g_str_equal (g_udev_device_get_subsystem (device), BACKLIGHT_SUBSYSTEM) && + g_str_equal (action, "change")) { + g_signal_emit (self, signals[BACKLIGHT_CHANGED], 0, device); + } +} + + +static void +phosh_udev_manager_finalize (GObject *object) +{ + PhoshUdevManager *self = PHOSH_UDEV_MANAGER (object); + + g_clear_object (&self->session_proxy); + g_clear_object (&self->udev_client); + + G_OBJECT_CLASS (phosh_udev_manager_parent_class)->finalize (object); +} + + +static void +phosh_udev_manager_class_init (PhoshUdevManagerClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = phosh_udev_manager_finalize; + + signals[BACKLIGHT_CHANGED] = g_signal_new ("backlight-changed", + G_TYPE_FROM_CLASS (object_class), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, + 1, + G_UDEV_TYPE_DEVICE); +} + + +static void +phosh_udev_manager_init (PhoshUdevManager *self) +{ + const char * const subsystems[] = { BACKLIGHT_SUBSYSTEM, LEDS_SUBSYSTEM, NULL }; + g_autoptr (GError) err = NULL; + + self->udev_client = g_udev_client_new (subsystems); + g_signal_connect_object (self->udev_client, + "uevent", + G_CALLBACK (on_uevent), + self, + G_CONNECT_SWAPPED); + + /* This happens before any UI is up so a sync call is o.k. */ + self->session_proxy = + phosh_dbus_login_session_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + BUS_NAME, + OBJECT_PATH, + NULL, + &err); + if (!self->session_proxy) + g_debug ("Failed to get login1 session proxy: %s", err->message); +} + + +PhoshUdevManager * +phosh_udev_manager_get_default (void) +{ + static PhoshUdevManager *instance; + + if (instance == NULL) { + instance = g_object_new (PHOSH_TYPE_UDEV_MANAGER, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + return instance; +} + +/** + * phosh_udev_manager_find_backlight: + * @self: The udev maanger + * @connector_name: The connector name + * + * Get the backlight corresponding to the given connector name + * + * Returns:(transfer full): The list of torch devices + */ +GUdevDevice * +phosh_udev_manager_find_backlight (PhoshUdevManager *self, const char *connector_name) +{ + g_autolist (GUdevDevice) devices = NULL; + PhoshMonitorConnectorType connector_type; + GUdevDevice *device; + gboolean is_builtin; + + g_return_val_if_fail (PHOSH_IS_UDEV_MANAGER (self), NULL); + + connector_type = phosh_monitor_connector_type_from_name (connector_name); + is_builtin = phosh_monitor_connector_is_builtin (connector_type); + + if (phosh_shell_get_debug_flags () & PHOSH_SHELL_DEBUG_FLAG_FAKE_BUILTIN) + is_builtin = TRUE; + + devices = g_udev_client_query_by_subsystem (self->udev_client, BACKLIGHT_SUBSYSTEM); + if (!devices) + return NULL; + + /* Prefer the types firmware -> platform -> raw (see g-s-d < 49) */ + if (is_builtin) { + device = phosh_backlight_sysfs_udev_get_type (devices, "firmware"); + if (device) + return device; + + device = phosh_backlight_sysfs_udev_get_type (devices, "platform"); + if (device) + return device; + } + + device = phosh_backlight_sysfs_udev_get_raw (devices, connector_name); + if (device) + return device; + + if (is_builtin) + device = phosh_backlight_sysfs_udev_get_type (devices, "raw"); + + return device; +} + +/** + * phosh_udev_manager_find_torches: + * @self: The udev maanger + * + * Get the torch devices in the the system + * + * Returns:(transfer full): The list of torch devices + */ +GList * +phosh_udev_manager_find_torches (PhoshUdevManager *self, GError **err) +{ + g_autoptr (GUdevEnumerator) udev_enumerator = NULL; + g_autolist (GUdevDevice) device_list = NULL; + + udev_enumerator = g_udev_enumerator_new (self->udev_client); + g_udev_enumerator_add_match_subsystem (udev_enumerator, LEDS_SUBSYSTEM); + g_udev_enumerator_add_match_name (udev_enumerator, "*:torch"); + g_udev_enumerator_add_match_name (udev_enumerator, "*:flash"); + + device_list = g_udev_enumerator_execute (udev_enumerator); + if (!device_list) { + g_set_error (err, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Failed to enumerate LED devices"); + return NULL; + } + + return g_steal_pointer (&device_list); +} + +/** + * phosh_udev_manager_get_session_proxy: + * @self: The manager + * + * Get Logind's session manager. + * + * Returns: (transfer full)(nullable): The session manager + */ +PhoshDBusLoginSession * +phosh_udev_manager_get_session_proxy (PhoshUdevManager *self) +{ + g_return_val_if_fail (PHOSH_IS_UDEV_MANAGER (self), NULL); + + if (self->session_proxy) + return g_object_ref (self->session_proxy); + + return NULL; +} diff --git a/src/udev-manager.h b/src/udev-manager.h new file mode 100644 index 000000000..44bac37ef --- /dev/null +++ b/src/udev-manager.h @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 Phosh.mobi e.V. + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include "dbus/login1-session-dbus.h" + +#include + +#include + +G_BEGIN_DECLS + +#define PHOSH_TYPE_UDEV_MANAGER (phosh_udev_manager_get_type ()) + +G_DECLARE_FINAL_TYPE (PhoshUdevManager, phosh_udev_manager, PHOSH, UDEV_MANAGER, GObject) + +PhoshUdevManager * phosh_udev_manager_get_default (void); +GUdevDevice * phosh_udev_manager_find_backlight (PhoshUdevManager *self, + const char *connector_name); +GList * phosh_udev_manager_find_torches (PhoshUdevManager *self, GError **err); +PhoshDBusLoginSession *phosh_udev_manager_get_session_proxy (PhoshUdevManager *self); + +G_END_DECLS diff --git a/src/ui/activity.ui b/src/ui/activity.ui new file mode 100644 index 000000000..656fe3681 --- /dev/null +++ b/src/ui/activity.ui @@ -0,0 +1,138 @@ + + + + + diff --git a/src/ui/app-auth-prompt.ui b/src/ui/app-auth-prompt.ui new file mode 100644 index 000000000..ad2c91351 --- /dev/null +++ b/src/ui/app-auth-prompt.ui @@ -0,0 +1,80 @@ + + + + + diff --git a/src/ui/app-grid-base-button.ui b/src/ui/app-grid-base-button.ui new file mode 100644 index 000000000..05453ed22 --- /dev/null +++ b/src/ui/app-grid-base-button.ui @@ -0,0 +1,44 @@ + + + + + diff --git a/src/ui/app-grid-button.ui b/src/ui/app-grid-button.ui new file mode 100644 index 000000000..03767ea53 --- /dev/null +++ b/src/ui/app-grid-button.ui @@ -0,0 +1,58 @@ + + + + + + 1 + 64 + app-icon-unknown + + + 1 + PhoshAppGridButton + capture + + + + 3 + PhoshAppGridButton + + + + PhoshAppGridButton + + +
+
+ + Remove from _Favorites + app-btn.favorite-remove + action-disabled + + + Add to _Favorites + app-btn.favorite-add + action-disabled + + + View _Details + app-btn.view-details + action-disabled + + + Uninstall + app-btn.uninstall + action-disabled + +
+
+ + _Remove from Folder + app-btn.folder-remove + action-disabled + +
+
diff --git a/src/ui/app-grid-folder-button.ui b/src/ui/app-grid-folder-button.ui new file mode 100644 index 000000000..d012cd9fb --- /dev/null +++ b/src/ui/app-grid-folder-button.ui @@ -0,0 +1,23 @@ + + + + + + 1 + 1 + center + center + 8 + 8 + 1 + 1 + + 64 + + diff --git a/src/ui/app-grid.ui b/src/ui/app-grid.ui new file mode 100644 index 000000000..02b309525 --- /dev/null +++ b/src/ui/app-grid.ui @@ -0,0 +1,298 @@ + + + + + + diff --git a/src/ui/audio-device-row.ui b/src/ui/audio-device-row.ui new file mode 100644 index 000000000..23680db41 --- /dev/null +++ b/src/ui/audio-device-row.ui @@ -0,0 +1,41 @@ + + + + + diff --git a/src/ui/audio-settings.ui b/src/ui/audio-settings.ui new file mode 100644 index 000000000..efb5243d8 --- /dev/null +++ b/src/ui/audio-settings.ui @@ -0,0 +1,148 @@ + + + + + diff --git a/src/ui/brightness-settings.ui b/src/ui/brightness-settings.ui new file mode 100644 index 000000000..df310916f --- /dev/null +++ b/src/ui/brightness-settings.ui @@ -0,0 +1,132 @@ + + + + + diff --git a/src/ui/bt-device-row.ui b/src/ui/bt-device-row.ui new file mode 100644 index 000000000..c44df8de7 --- /dev/null +++ b/src/ui/bt-device-row.ui @@ -0,0 +1,34 @@ + + + + + + diff --git a/src/ui/bt-status-page.ui b/src/ui/bt-status-page.ui new file mode 100644 index 000000000..8eb94ea67 --- /dev/null +++ b/src/ui/bt-status-page.ui @@ -0,0 +1,58 @@ + + + + + + 1 + crossfade + 0 + + + 1 + none + + + + devices + + + + + 1 + bluetooth-disabled + enable_button + + + empty-state + + + + + 1 + Enable Bluetooth + center + 0 + + + + 1 + 1 + panel.launch-panel + ("bluetooth", [<"">]) + 0 + + + 1 + end + Bluetooth Settings + + + + diff --git a/src/ui/call-notification.ui b/src/ui/call-notification.ui new file mode 100644 index 000000000..8b6dbd66f --- /dev/null +++ b/src/ui/call-notification.ui @@ -0,0 +1,120 @@ + + + + + diff --git a/src/ui/cell-broadcast-prompt.ui b/src/ui/cell-broadcast-prompt.ui new file mode 100644 index 000000000..5579aaf86 --- /dev/null +++ b/src/ui/cell-broadcast-prompt.ui @@ -0,0 +1,30 @@ + + + + + diff --git a/src/ui/channel-bar.ui b/src/ui/channel-bar.ui new file mode 100644 index 000000000..68a1e5864 --- /dev/null +++ b/src/ui/channel-bar.ui @@ -0,0 +1,33 @@ + + + + + + + + diff --git a/src/ui/emergency-contact-row.ui b/src/ui/emergency-contact-row.ui new file mode 100644 index 000000000..338c6a498 --- /dev/null +++ b/src/ui/emergency-contact-row.ui @@ -0,0 +1,15 @@ + + + + + + diff --git a/src/ui/emergency-menu.ui b/src/ui/emergency-menu.ui new file mode 100644 index 000000000..3a7af9f09 --- /dev/null +++ b/src/ui/emergency-menu.ui @@ -0,0 +1,159 @@ + + + + + + diff --git a/src/ui/end-session-dialog.ui b/src/ui/end-session-dialog.ui new file mode 100644 index 000000000..87989e7e0 --- /dev/null +++ b/src/ui/end-session-dialog.ui @@ -0,0 +1,79 @@ + + + + + diff --git a/src/ui/feedback-status-page.ui b/src/ui/feedback-status-page.ui new file mode 100644 index 000000000..41f920735 --- /dev/null +++ b/src/ui/feedback-status-page.ui @@ -0,0 +1,57 @@ + + + + + + 1 + + + 1 + none + + + 1 + Do not disturb + 1 + + + + 1 + 16 + + + + + 1 + center + + + + + + + + + + 1 + 1 + panel.launch-mobile-panel + + ("feedback", [<"">]) + 0 + + + 1 + end + Feedback Settings + + + + diff --git a/src/ui/gtk-mount-prompt.ui b/src/ui/gtk-mount-prompt.ui new file mode 100644 index 000000000..e8d590800 --- /dev/null +++ b/src/ui/gtk-mount-prompt.ui @@ -0,0 +1,143 @@ + + + + + + diff --git a/src/ui/home.ui b/src/ui/home.ui new file mode 100644 index 000000000..583061dbd --- /dev/null +++ b/src/ui/home.ui @@ -0,0 +1,77 @@ + + + + + + evbox_home_bar + + + + powerbar + + + + + + diff --git a/src/ui/keypad.ui b/src/ui/keypad.ui new file mode 100644 index 000000000..68c92da06 --- /dev/null +++ b/src/ui/keypad.ui @@ -0,0 +1,213 @@ + + + + + diff --git a/src/ui/lockscreen.ui b/src/ui/lockscreen.ui new file mode 100644 index 000000000..0eb6242a1 --- /dev/null +++ b/src/ui/lockscreen.ui @@ -0,0 +1,384 @@ + + + + + + + PhoshLockscreen + capture + 1 + 2 + + + + + 1 + 1 + 0 + + + + 1 + edit-clear-symbolic + + + + + 1 + 1 + + + + 1 + input-keyboard-symbolic + + + + + btn_delete + + + + diff --git a/src/ui/media-player.ui b/src/ui/media-player.ui new file mode 100644 index 000000000..2a8baa607 --- /dev/null +++ b/src/ui/media-player.ui @@ -0,0 +1,230 @@ + + + + + diff --git a/src/ui/network-auth-prompt.ui b/src/ui/network-auth-prompt.ui new file mode 100644 index 000000000..3727546d0 --- /dev/null +++ b/src/ui/network-auth-prompt.ui @@ -0,0 +1,99 @@ + + + + + + + 1 + 12 + + + 1 + end + Password: + + + 0 + 0 + + + + + 1 + 1 + center + 1 + 1 + password_buffer + + + + 1 + 0 + + + + + + 1 + 12 + + + + diff --git a/src/ui/notification-content.ui b/src/ui/notification-content.ui new file mode 100644 index 000000000..3b08a2d4b --- /dev/null +++ b/src/ui/notification-content.ui @@ -0,0 +1,84 @@ + + + + + diff --git a/src/ui/notification-frame.ui b/src/ui/notification-frame.ui new file mode 100644 index 000000000..472f71c5b --- /dev/null +++ b/src/ui/notification-frame.ui @@ -0,0 +1,101 @@ + + + + + + header + + + + + list_notifs + + + + diff --git a/src/ui/osd-window.ui b/src/ui/osd-window.ui new file mode 100644 index 000000000..adab838a8 --- /dev/null +++ b/src/ui/osd-window.ui @@ -0,0 +1,66 @@ + + + + + + PhoshOsdWindow + + + diff --git a/src/ui/overview.ui b/src/ui/overview.ui new file mode 100644 index 000000000..fc2cfb4fc --- /dev/null +++ b/src/ui/overview.ui @@ -0,0 +1,35 @@ + + + + + + diff --git a/src/ui/password-entry.ui b/src/ui/password-entry.ui new file mode 100644 index 000000000..f3208128a --- /dev/null +++ b/src/ui/password-entry.ui @@ -0,0 +1,12 @@ + + + + + diff --git a/src/ui/polkit-auth-prompt.ui b/src/ui/polkit-auth-prompt.ui new file mode 100644 index 000000000..540a1b08a --- /dev/null +++ b/src/ui/polkit-auth-prompt.ui @@ -0,0 +1,113 @@ + + + + + diff --git a/src/ui/power-menu.ui b/src/ui/power-menu.ui new file mode 100644 index 000000000..6c7926871 --- /dev/null +++ b/src/ui/power-menu.ui @@ -0,0 +1,260 @@ + + + + + diff --git a/src/ui/quick-setting.ui b/src/ui/quick-setting.ui new file mode 100644 index 000000000..88b7c99a6 --- /dev/null +++ b/src/ui/quick-setting.ui @@ -0,0 +1,56 @@ + + + + + + primary + + + + 3 + primary + + + diff --git a/src/ui/quick-settings-box.ui b/src/ui/quick-settings-box.ui new file mode 100644 index 000000000..ade47dddd --- /dev/null +++ b/src/ui/quick-settings-box.ui @@ -0,0 +1,9 @@ + + + +