From e4283f6d48b98e764b988b43bbc86b9d52e6ec94 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:54:43 +0200 Subject: Adding upstream version 43.9. Signed-off-by: Daniel Baumann --- src/calendar-server/README | 1 + src/calendar-server/calendar-debug.h | 50 + src/calendar-server/calendar-sources.c | 506 +++ src/calendar-server/calendar-sources.h | 64 + src/calendar-server/evolution-calendar.desktop.in | 8 + src/calendar-server/gnome-shell-calendar-server.c | 1131 +++++ src/calendar-server/meson.build | 37 + .../org.gnome.Shell.CalendarServer.service.in | 3 + src/data-to-c.pl | 37 + src/gnome-shell-extension-prefs | 31 + src/gnome-shell-extension-tool.in | 59 + src/gnome-shell-perf-tool.in | 326 ++ src/gnome-shell-plugin.c | 394 ++ src/gnome-shell-portal-helper.c | 52 + src/gtkactionmuxer.c | 945 ++++ src/gtkactionmuxer.h | 64 + src/gtkactionobservable.c | 78 + src/gtkactionobservable.h | 60 + src/gtkactionobserver.c | 189 + src/gtkactionobserver.h | 91 + src/hotplug-sniffer/hotplug-mimetypes.h | 141 + src/hotplug-sniffer/hotplug-sniffer.c | 298 ++ src/hotplug-sniffer/meson.build | 22 + .../org.gnome.Shell.HotplugSniffer.service.in | 3 + src/hotplug-sniffer/shell-mime-sniffer.c | 590 +++ src/hotplug-sniffer/shell-mime-sniffer.h | 46 + src/main.c | 599 +++ src/meson.build | 276 ++ src/org.gtk.Application.xml | 19 + src/run-js-test.c | 118 + src/shell-action-modes.h | 35 + src/shell-app-cache-private.h | 19 + src/shell-app-cache.c | 404 ++ src/shell-app-private.h | 24 + src/shell-app-system-private.h | 9 + src/shell-app-system.c | 586 +++ src/shell-app-system.h | 32 + src/shell-app-usage.c | 774 ++++ src/shell-app-usage.h | 23 + src/shell-app.c | 1756 ++++++++ src/shell-app.h | 83 + src/shell-blur-effect.c | 907 ++++ src/shell-blur-effect.h | 57 + src/shell-embedded-window-private.h | 20 + src/shell-embedded-window.c | 247 ++ src/shell-embedded-window.h | 19 + src/shell-global-private.h | 23 + src/shell-global.c | 1860 ++++++++ src/shell-global.h | 94 + src/shell-glsl-effect.c | 205 + src/shell-glsl-effect.h | 62 + src/shell-gtk-embed.c | 364 ++ src/shell-gtk-embed.h | 20 + src/shell-invert-lightness-effect.c | 152 + src/shell-invert-lightness-effect.h | 41 + src/shell-keyring-prompt.c | 832 ++++ src/shell-keyring-prompt.h | 57 + src/shell-mount-operation.c | 189 + src/shell-mount-operation.h | 41 + src/shell-network-agent.c | 899 ++++ src/shell-network-agent.h | 73 + src/shell-perf-helper.c | 376 ++ src/shell-perf-log.c | 959 +++++ src/shell-perf-log.h | 75 + src/shell-polkit-authentication-agent.c | 429 ++ src/shell-polkit-authentication-agent.h | 35 + src/shell-screenshot.c | 1217 ++++++ src/shell-screenshot.h | 90 + src/shell-secure-text-buffer.c | 191 + src/shell-secure-text-buffer.h | 39 + src/shell-square-bin.c | 43 + src/shell-square-bin.h | 13 + src/shell-stack.c | 196 + src/shell-stack.h | 11 + src/shell-tray-icon.c | 294 ++ src/shell-tray-icon.h | 16 + src/shell-tray-manager.c | 374 ++ src/shell-tray-manager.h | 22 + src/shell-util.c | 854 ++++ src/shell-util.h | 93 + src/shell-window-preview-layout.c | 495 +++ src/shell-window-preview-layout.h | 33 + src/shell-window-preview.c | 177 + src/shell-window-preview.h | 14 + src/shell-window-tracker-private.h | 11 + src/shell-window-tracker.c | 811 ++++ src/shell-window-tracker.h | 29 + src/shell-wm-private.h | 63 + src/shell-wm.c | 462 ++ src/shell-wm.h | 32 + src/shell-workspace-background.c | 226 + src/shell-workspace-background.h | 14 + src/st/croco/cr-additional-sel.c | 498 +++ src/st/croco/cr-additional-sel.h | 98 + src/st/croco/cr-attr-sel.c | 234 + src/st/croco/cr-attr-sel.h | 74 + src/st/croco/cr-cascade.c | 215 + src/st/croco/cr-cascade.h | 74 + src/st/croco/cr-declaration.c | 798 ++++ src/st/croco/cr-declaration.h | 136 + src/st/croco/cr-doc-handler.c | 276 ++ src/st/croco/cr-doc-handler.h | 298 ++ src/st/croco/cr-enc-handler.c | 184 + src/st/croco/cr-enc-handler.h | 94 + src/st/croco/cr-fonts.c | 948 ++++ src/st/croco/cr-fonts.h | 315 ++ src/st/croco/cr-input.c | 1191 +++++ src/st/croco/cr-input.h | 174 + src/st/croco/cr-num.c | 313 ++ src/st/croco/cr-num.h | 127 + src/st/croco/cr-om-parser.c | 1142 +++++ src/st/croco/cr-om-parser.h | 98 + src/st/croco/cr-parser.c | 4539 ++++++++++++++++++++ src/st/croco/cr-parser.h | 128 + src/st/croco/cr-parsing-location.c | 171 + src/st/croco/cr-parsing-location.h | 70 + src/st/croco/cr-prop-list.c | 404 ++ src/st/croco/cr-prop-list.h | 80 + src/st/croco/cr-pseudo.c | 166 + src/st/croco/cr-pseudo.h | 64 + src/st/croco/cr-rgb.c | 604 +++ src/st/croco/cr-rgb.h | 84 + src/st/croco/cr-selector.c | 305 ++ src/st/croco/cr-selector.h | 95 + src/st/croco/cr-simple-sel.c | 323 ++ src/st/croco/cr-simple-sel.h | 130 + src/st/croco/cr-statement.c | 2784 ++++++++++++ src/st/croco/cr-statement.h | 440 ++ src/st/croco/cr-string.c | 168 + src/st/croco/cr-string.h | 76 + src/st/croco/cr-stylesheet.c | 177 + src/st/croco/cr-stylesheet.h | 102 + src/st/croco/cr-term.c | 786 ++++ src/st/croco/cr-term.h | 190 + src/st/croco/cr-tknzr.c | 2762 ++++++++++++ src/st/croco/cr-tknzr.h | 115 + src/st/croco/cr-token.c | 636 +++ src/st/croco/cr-token.h | 212 + src/st/croco/cr-utils.c | 1330 ++++++ src/st/croco/cr-utils.h | 246 ++ src/st/croco/libcroco-config.h | 13 + src/st/croco/libcroco.h | 42 + src/st/meson.build | 220 + src/st/st-adjustment.c | 1013 +++++ src/st/st-adjustment.h | 92 + src/st/st-bin.c | 404 ++ src/st/st-bin.h | 53 + src/st/st-border-image.c | 171 + src/st/st-border-image.h | 54 + src/st/st-box-layout.c | 307 ++ src/st/st-box-layout.h | 65 + src/st/st-button.c | 1043 +++++ src/st/st-button.h | 85 + src/st/st-clipboard.c | 348 ++ src/st/st-clipboard.h | 105 + src/st/st-drawing-area.c | 242 ++ src/st/st-drawing-area.h | 44 + src/st/st-entry.c | 1626 +++++++ src/st/st-entry.h | 79 + src/st/st-focus-manager.c | 256 ++ src/st/st-focus-manager.h | 65 + src/st/st-generic-accessible.c | 246 ++ src/st/st-generic-accessible.h | 62 + src/st/st-icon-colors.c | 133 + src/st/st-icon-colors.h | 43 + src/st/st-icon.c | 833 ++++ src/st/st-icon.h | 82 + src/st/st-image-content.c | 346 ++ src/st/st-image-content.h | 33 + src/st/st-label.c | 549 +++ src/st/st-label.h | 58 + src/st/st-password-entry.c | 363 ++ src/st/st-password-entry.h | 46 + src/st/st-private.c | 804 ++++ src/st/st-private.h | 75 + src/st/st-scroll-bar.c | 1014 +++++ src/st/st-scroll-bar.h | 53 + src/st/st-scroll-view-fade.c | 461 ++ src/st/st-scroll-view-fade.glsl | 77 + src/st/st-scroll-view-fade.h | 36 + src/st/st-scroll-view.c | 1327 ++++++ src/st/st-scroll-view.h | 90 + src/st/st-scrollable.c | 196 + src/st/st-scrollable.h | 59 + src/st/st-settings.c | 451 ++ src/st/st-settings.h | 42 + src/st/st-shadow.c | 307 ++ src/st/st-shadow.h | 95 + src/st/st-texture-cache.c | 1688 ++++++++ src/st/st-texture-cache.h | 120 + src/st/st-theme-context.c | 492 +++ src/st/st-theme-context.h | 65 + src/st/st-theme-node-drawing.c | 2864 ++++++++++++ src/st/st-theme-node-private.h | 131 + src/st/st-theme-node-transition.c | 470 ++ src/st/st-theme-node-transition.h | 58 + src/st/st-theme-node.c | 4325 +++++++++++++++++++ src/st/st-theme-node.h | 368 ++ src/st/st-theme-private.h | 41 + src/st/st-theme.c | 1085 +++++ src/st/st-theme.h | 51 + src/st/st-types.h | 52 + src/st/st-viewport.c | 600 +++ src/st/st-viewport.h | 40 + src/st/st-widget-accessible.h | 76 + src/st/st-widget.c | 3049 +++++++++++++ src/st/st-widget.h | 167 + src/st/st.h.in | 3 + src/st/test-theme.c | 637 +++ src/st/test-theme.css | 107 + src/tray/meson.build | 12 + src/tray/na-tray-child.c | 504 +++ src/tray/na-tray-child.h | 66 + src/tray/na-tray-manager.c | 888 ++++ src/tray/na-tray-manager.h | 106 + 215 files changed, 80586 insertions(+) create mode 100644 src/calendar-server/README create mode 100644 src/calendar-server/calendar-debug.h create mode 100644 src/calendar-server/calendar-sources.c create mode 100644 src/calendar-server/calendar-sources.h create mode 100644 src/calendar-server/evolution-calendar.desktop.in create mode 100644 src/calendar-server/gnome-shell-calendar-server.c create mode 100644 src/calendar-server/meson.build create mode 100644 src/calendar-server/org.gnome.Shell.CalendarServer.service.in create mode 100755 src/data-to-c.pl create mode 100755 src/gnome-shell-extension-prefs create mode 100755 src/gnome-shell-extension-tool.in create mode 100755 src/gnome-shell-perf-tool.in create mode 100644 src/gnome-shell-plugin.c create mode 100644 src/gnome-shell-portal-helper.c create mode 100644 src/gtkactionmuxer.c create mode 100644 src/gtkactionmuxer.h create mode 100644 src/gtkactionobservable.c create mode 100644 src/gtkactionobservable.h create mode 100644 src/gtkactionobserver.c create mode 100644 src/gtkactionobserver.h create mode 100644 src/hotplug-sniffer/hotplug-mimetypes.h create mode 100644 src/hotplug-sniffer/hotplug-sniffer.c create mode 100644 src/hotplug-sniffer/meson.build create mode 100644 src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in create mode 100644 src/hotplug-sniffer/shell-mime-sniffer.c create mode 100644 src/hotplug-sniffer/shell-mime-sniffer.h create mode 100644 src/main.c create mode 100644 src/meson.build create mode 100644 src/org.gtk.Application.xml create mode 100644 src/run-js-test.c create mode 100644 src/shell-action-modes.h create mode 100644 src/shell-app-cache-private.h create mode 100644 src/shell-app-cache.c create mode 100644 src/shell-app-private.h create mode 100644 src/shell-app-system-private.h create mode 100644 src/shell-app-system.c create mode 100644 src/shell-app-system.h create mode 100644 src/shell-app-usage.c create mode 100644 src/shell-app-usage.h create mode 100644 src/shell-app.c create mode 100644 src/shell-app.h create mode 100644 src/shell-blur-effect.c create mode 100644 src/shell-blur-effect.h create mode 100644 src/shell-embedded-window-private.h create mode 100644 src/shell-embedded-window.c create mode 100644 src/shell-embedded-window.h create mode 100644 src/shell-global-private.h create mode 100644 src/shell-global.c create mode 100644 src/shell-global.h create mode 100644 src/shell-glsl-effect.c create mode 100644 src/shell-glsl-effect.h create mode 100644 src/shell-gtk-embed.c create mode 100644 src/shell-gtk-embed.h create mode 100644 src/shell-invert-lightness-effect.c create mode 100644 src/shell-invert-lightness-effect.h create mode 100644 src/shell-keyring-prompt.c create mode 100644 src/shell-keyring-prompt.h create mode 100644 src/shell-mount-operation.c create mode 100644 src/shell-mount-operation.h create mode 100644 src/shell-network-agent.c create mode 100644 src/shell-network-agent.h create mode 100644 src/shell-perf-helper.c create mode 100644 src/shell-perf-log.c create mode 100644 src/shell-perf-log.h create mode 100644 src/shell-polkit-authentication-agent.c create mode 100644 src/shell-polkit-authentication-agent.h create mode 100644 src/shell-screenshot.c create mode 100644 src/shell-screenshot.h create mode 100644 src/shell-secure-text-buffer.c create mode 100644 src/shell-secure-text-buffer.h create mode 100644 src/shell-square-bin.c create mode 100644 src/shell-square-bin.h create mode 100644 src/shell-stack.c create mode 100644 src/shell-stack.h create mode 100644 src/shell-tray-icon.c create mode 100644 src/shell-tray-icon.h create mode 100644 src/shell-tray-manager.c create mode 100644 src/shell-tray-manager.h create mode 100644 src/shell-util.c create mode 100644 src/shell-util.h create mode 100644 src/shell-window-preview-layout.c create mode 100644 src/shell-window-preview-layout.h create mode 100644 src/shell-window-preview.c create mode 100644 src/shell-window-preview.h create mode 100644 src/shell-window-tracker-private.h create mode 100644 src/shell-window-tracker.c create mode 100644 src/shell-window-tracker.h create mode 100644 src/shell-wm-private.h create mode 100644 src/shell-wm.c create mode 100644 src/shell-wm.h create mode 100644 src/shell-workspace-background.c create mode 100644 src/shell-workspace-background.h create mode 100644 src/st/croco/cr-additional-sel.c create mode 100644 src/st/croco/cr-additional-sel.h create mode 100644 src/st/croco/cr-attr-sel.c create mode 100644 src/st/croco/cr-attr-sel.h create mode 100644 src/st/croco/cr-cascade.c create mode 100644 src/st/croco/cr-cascade.h create mode 100644 src/st/croco/cr-declaration.c create mode 100644 src/st/croco/cr-declaration.h create mode 100644 src/st/croco/cr-doc-handler.c create mode 100644 src/st/croco/cr-doc-handler.h create mode 100644 src/st/croco/cr-enc-handler.c create mode 100644 src/st/croco/cr-enc-handler.h create mode 100644 src/st/croco/cr-fonts.c create mode 100644 src/st/croco/cr-fonts.h create mode 100644 src/st/croco/cr-input.c create mode 100644 src/st/croco/cr-input.h create mode 100644 src/st/croco/cr-num.c create mode 100644 src/st/croco/cr-num.h create mode 100644 src/st/croco/cr-om-parser.c create mode 100644 src/st/croco/cr-om-parser.h create mode 100644 src/st/croco/cr-parser.c create mode 100644 src/st/croco/cr-parser.h create mode 100644 src/st/croco/cr-parsing-location.c create mode 100644 src/st/croco/cr-parsing-location.h create mode 100644 src/st/croco/cr-prop-list.c create mode 100644 src/st/croco/cr-prop-list.h create mode 100644 src/st/croco/cr-pseudo.c create mode 100644 src/st/croco/cr-pseudo.h create mode 100644 src/st/croco/cr-rgb.c create mode 100644 src/st/croco/cr-rgb.h create mode 100644 src/st/croco/cr-selector.c create mode 100644 src/st/croco/cr-selector.h create mode 100644 src/st/croco/cr-simple-sel.c create mode 100644 src/st/croco/cr-simple-sel.h create mode 100644 src/st/croco/cr-statement.c create mode 100644 src/st/croco/cr-statement.h create mode 100644 src/st/croco/cr-string.c create mode 100644 src/st/croco/cr-string.h create mode 100644 src/st/croco/cr-stylesheet.c create mode 100644 src/st/croco/cr-stylesheet.h create mode 100644 src/st/croco/cr-term.c create mode 100644 src/st/croco/cr-term.h create mode 100644 src/st/croco/cr-tknzr.c create mode 100644 src/st/croco/cr-tknzr.h create mode 100644 src/st/croco/cr-token.c create mode 100644 src/st/croco/cr-token.h create mode 100644 src/st/croco/cr-utils.c create mode 100644 src/st/croco/cr-utils.h create mode 100644 src/st/croco/libcroco-config.h create mode 100644 src/st/croco/libcroco.h create mode 100644 src/st/meson.build create mode 100644 src/st/st-adjustment.c create mode 100644 src/st/st-adjustment.h create mode 100644 src/st/st-bin.c create mode 100644 src/st/st-bin.h create mode 100644 src/st/st-border-image.c create mode 100644 src/st/st-border-image.h create mode 100644 src/st/st-box-layout.c create mode 100644 src/st/st-box-layout.h create mode 100644 src/st/st-button.c create mode 100644 src/st/st-button.h create mode 100644 src/st/st-clipboard.c create mode 100644 src/st/st-clipboard.h create mode 100644 src/st/st-drawing-area.c create mode 100644 src/st/st-drawing-area.h create mode 100644 src/st/st-entry.c create mode 100644 src/st/st-entry.h create mode 100644 src/st/st-focus-manager.c create mode 100644 src/st/st-focus-manager.h create mode 100644 src/st/st-generic-accessible.c create mode 100644 src/st/st-generic-accessible.h create mode 100644 src/st/st-icon-colors.c create mode 100644 src/st/st-icon-colors.h create mode 100644 src/st/st-icon.c create mode 100644 src/st/st-icon.h create mode 100644 src/st/st-image-content.c create mode 100644 src/st/st-image-content.h create mode 100644 src/st/st-label.c create mode 100644 src/st/st-label.h create mode 100644 src/st/st-password-entry.c create mode 100644 src/st/st-password-entry.h create mode 100644 src/st/st-private.c create mode 100644 src/st/st-private.h create mode 100644 src/st/st-scroll-bar.c create mode 100644 src/st/st-scroll-bar.h create mode 100644 src/st/st-scroll-view-fade.c create mode 100644 src/st/st-scroll-view-fade.glsl create mode 100644 src/st/st-scroll-view-fade.h create mode 100644 src/st/st-scroll-view.c create mode 100644 src/st/st-scroll-view.h create mode 100644 src/st/st-scrollable.c create mode 100644 src/st/st-scrollable.h create mode 100644 src/st/st-settings.c create mode 100644 src/st/st-settings.h create mode 100644 src/st/st-shadow.c create mode 100644 src/st/st-shadow.h create mode 100644 src/st/st-texture-cache.c create mode 100644 src/st/st-texture-cache.h create mode 100644 src/st/st-theme-context.c create mode 100644 src/st/st-theme-context.h create mode 100644 src/st/st-theme-node-drawing.c create mode 100644 src/st/st-theme-node-private.h create mode 100644 src/st/st-theme-node-transition.c create mode 100644 src/st/st-theme-node-transition.h create mode 100644 src/st/st-theme-node.c create mode 100644 src/st/st-theme-node.h create mode 100644 src/st/st-theme-private.h create mode 100644 src/st/st-theme.c create mode 100644 src/st/st-theme.h create mode 100644 src/st/st-types.h create mode 100644 src/st/st-viewport.c create mode 100644 src/st/st-viewport.h create mode 100644 src/st/st-widget-accessible.h create mode 100644 src/st/st-widget.c create mode 100644 src/st/st-widget.h create mode 100644 src/st/st.h.in create mode 100644 src/st/test-theme.c create mode 100644 src/st/test-theme.css create mode 100644 src/tray/meson.build create mode 100644 src/tray/na-tray-child.c create mode 100644 src/tray/na-tray-child.h create mode 100644 src/tray/na-tray-manager.c create mode 100644 src/tray/na-tray-manager.h (limited to 'src') diff --git a/src/calendar-server/README b/src/calendar-server/README new file mode 100644 index 0000000..ad9b5e3 --- /dev/null +++ b/src/calendar-server/README @@ -0,0 +1 @@ +Please keep in sync with gnome-panel. diff --git a/src/calendar-server/calendar-debug.h b/src/calendar-server/calendar-debug.h new file mode 100644 index 0000000..39befd7 --- /dev/null +++ b/src/calendar-server/calendar-debug.h @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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, see . + * + * 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/src/calendar-server/calendar-sources.c b/src/calendar-server/calendar-sources.c new file mode 100644 index 0000000..9c25f4e --- /dev/null +++ b/src/calendar-server/calendar-sources.c @@ -0,0 +1,506 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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, see . + * + * 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 gnome-shell-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 ("gnome-shell-calendar-server[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; +} diff --git a/src/calendar-server/calendar-sources.h b/src/calendar-server/calendar-sources.h new file mode 100644 index 0000000..1ffc8ad --- /dev/null +++ b/src/calendar-server/calendar-sources.h @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2004 Free Software Foundation, Inc. + * + * 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, see . + * + * 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/src/calendar-server/evolution-calendar.desktop.in b/src/calendar-server/evolution-calendar.desktop.in new file mode 100644 index 0000000..1e34997 --- /dev/null +++ b/src/calendar-server/evolution-calendar.desktop.in @@ -0,0 +1,8 @@ +[Desktop Entry] +Name=Evolution Calendar +Exec=evolution -c calendar +# Translators: Do NOT translate or transliterate this text (this is an icon file name)! +Icon=evolution +NoDisplay=true +Type=Application +StartupNotify=true diff --git a/src/calendar-server/gnome-shell-calendar-server.c b/src/calendar-server/gnome-shell-calendar-server.c new file mode 100644 index 0000000..4cd28d1 --- /dev/null +++ b/src/calendar-server/gnome-shell-calendar-server.c @@ -0,0 +1,1131 @@ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * 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 "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 "org.gnome.Shell.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; +} 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 *timezone = 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)) + timezone = 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), &timezone, NULL, NULL)) + print_debug ("Failed to get timezone '%s'\n", i_cal_parameter_get_tzid (param)); + + g_clear_object (¶m); + } + + if (timezone == NULL) + timezone = default_zone; + + i_cal_time_set_timezone (itt, timezone); + + g_clear_object (&prop); + + *out_itt = itt; + *out_timezone = timezone; + + return TRUE; +} + +static inline time_t +get_ical_start_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *timezone; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &timezone)) + { + return 0; + } + + retval = i_cal_time_as_timet_with_zone (itt, timezone); + + g_clear_object (&itt); + + return retval; +} + +static inline time_t +get_ical_end_time (ECalClient *cal, + ICalComponent *icomp, + ICalTimezone *default_zone) +{ + ICalTime *itt; + ICalTimezone *timezone; + time_t retval; + + if (!get_time_from_property (cal, + icomp, + I_CAL_DTEND_PROPERTY, + i_cal_property_get_dtend, + default_zone, + &itt, + &timezone)) + { + if (!get_time_from_property (cal, + icomp, + I_CAL_DTSTART_PROPERTY, + i_cal_property_get_dtstart, + default_zone, + &itt, + &timezone)) + { + 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, timezone); + + g_clear_object (&itt); + + return retval; +} + +static CalendarAppointment * +calendar_appointment_new (ECalClient *cal, + ECalComponent *comp) +{ + CalendarAppointment *appt; + ICalTimezone *default_zone; + ICalComponent *ical; + ECalComponentId *id; + + 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); + + 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); + } +} + +static time_t +timet_from_ical_time (ICalTime *time, + ICalTimezone *default_zone) +{ + ICalTimezone *timezone = NULL; + + timezone = i_cal_time_get_timezone (time); + if (timezone == NULL) + timezone = default_zone; + return i_cal_time_as_timet_with_zone (time, timezone); +} + +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, extras_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; + + 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}")); + 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 */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.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 */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.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; + g_autoptr(GHashTable) covered_uids = NULL; + GSList *link; + gboolean expand_recurrences; + + cal_client = e_cal_client_view_ref_client (view); + covered_uids = g_hash_table_new (g_str_hash, g_str_equal); + 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; + const gchar *uid; + gboolean fallback = FALSE; + + if (!icomp) + continue; + + uid = i_cal_component_get_uid (icomp); + if (!uid || g_hash_table_contains (covered_uids, uid)) + continue; + + g_hash_table_add (covered_uids, (gpointer) uid); + + 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 if (expand_recurrences && + e_cal_util_component_is_instance (icomp)) + { + ICalComponent *main_comp = NULL; + + /* Always pass whole series of the recurring events, because + * the calendar removes events with the same UID first. */ + if (e_cal_client_get_object_sync (cal_client, uid, NULL, &main_comp, NULL, NULL)) + { + CollectAppointmentsData data; + + data.client = cal_client; + data.pappointments = &app->notify_appointments; + + e_cal_client_generate_instances_for_object_sync (cal_client, main_comp, app->since, app->until, NULL, + generate_instances_cb, &data); + + g_clear_object (&main_comp); + } + else + { + fallback = TRUE; + } + } + else + { + fallback = TRUE; + } + + if (fallback) + { + 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); + + 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, + "/org/gnome/Shell/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + "org.gnome.Shell.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 *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); + 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 */ + "/org/gnome/Shell/CalendarServer", + "org.gnome.Shell.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, + "org.gnome.Shell.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 */ + "/org/gnome/Shell/CalendarServer", + "org.freedesktop.DBus.Properties", + "PropertiesChanged", + g_variant_new ("(sa{sv}as)", + "org.gnome.Shell.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, + "/org/gnome/Shell/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 ("gnome-shell-calendar-server[%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 ("gnome-shell-calendar-server[%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 ("gnome-shell 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/src/calendar-server/meson.build b/src/calendar-server/meson.build new file mode 100644 index 0000000..8b4ef41 --- /dev/null +++ b/src/calendar-server/meson.build @@ -0,0 +1,37 @@ +calendar_sources = [ + 'gnome-shell-calendar-server.c', + 'calendar-debug.h', + 'calendar-sources.c', + 'calendar-sources.h' +] + +calendar_server = executable('gnome-shell-calendar-server', 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"' + ], + install_dir: libexecdir, + install: true +) + +service_file = 'org.gnome.Shell.CalendarServer.service' + +configure_file( + input: service_file + '.in', + output: service_file, + configuration: service_data, + install_dir: servicedir +) + +i18n.merge_file( + input: 'evolution-calendar.desktop.in', + output: 'evolution-calendar.desktop', + po_dir: po_dir, + install: true, + install_dir: desktopdir, + type: 'desktop' +) diff --git a/src/calendar-server/org.gnome.Shell.CalendarServer.service.in b/src/calendar-server/org.gnome.Shell.CalendarServer.service.in new file mode 100644 index 0000000..5addce6 --- /dev/null +++ b/src/calendar-server/org.gnome.Shell.CalendarServer.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Shell.CalendarServer +Exec=@libexecdir@/gnome-shell-calendar-server diff --git a/src/data-to-c.pl b/src/data-to-c.pl new file mode 100755 index 0000000..69f7436 --- /dev/null +++ b/src/data-to-c.pl @@ -0,0 +1,37 @@ +#!/usr/bin/env perl + +# Copyright © 2011 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 of the licence, 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 . +# +# Author: Kalev Lember + + +if (@ARGV != 2) { + die "Usage: data-to-c.pl \n"; +} + +$file = $ARGV[0]; + +open (FILE, $file) || die "Cannot open $file: $!\n"; + +printf ("const char %s[] = \"", $ARGV[1]); +while (my $line = ) { + foreach my $c (split //, $line) { + printf ("\\x%02x", ord ($c)); + } +} +print "\";\n"; + +close (FILE); diff --git a/src/gnome-shell-extension-prefs b/src/gnome-shell-extension-prefs new file mode 100755 index 0000000..303b196 --- /dev/null +++ b/src/gnome-shell-extension-prefs @@ -0,0 +1,31 @@ +#!/bin/sh + +openPrefs() { + if [ "$(which gnome-extensions)" ] + then + gnome-extensions prefs $1 + else + gdbus call --session \ + --dest=org.gnome.Shell.Extensions \ + --object-path=/org/gnome/Shell/Extensions \ + --method=org.gnome.Shell.Extensions.OpenExtensionPrefs $1 '' '{}' + fi +} + +cat >&2 < + * + * 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, see . + */ + +/* + * GnomeShellPlugin is the entry point for for GNOME Shell into and out of + * Mutter. By registering itself into Mutter using + * meta_plugin_manager_set_plugin_type(), Mutter will call the vfuncs of the + * plugin at the appropriate time. + * + * The functions in in GnomeShellPlugin are all just stubs, which just call the + * similar methods in GnomeShellWm. + */ + +#include "config.h" + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "shell-wm-private.h" + +#define GNOME_TYPE_SHELL_PLUGIN (gnome_shell_plugin_get_type ()) +G_DECLARE_FINAL_TYPE (GnomeShellPlugin, gnome_shell_plugin, + GNOME, SHELL_PLUGIN, + MetaPlugin) + +struct _GnomeShellPlugin +{ + MetaPlugin parent; + + int glx_error_base; + int glx_event_base; + guint have_swap_event : 1; + CoglContext *cogl_context; + + ShellGlobal *global; +}; + +G_DEFINE_TYPE (GnomeShellPlugin, gnome_shell_plugin, META_TYPE_PLUGIN) + +static gboolean +gnome_shell_plugin_has_swap_event (GnomeShellPlugin *shell_plugin) +{ + CoglDisplay *cogl_display = + cogl_context_get_display (shell_plugin->cogl_context); + CoglRenderer *renderer = cogl_display_get_renderer (cogl_display); + const char * (* query_extensions_string) (Display *dpy, int screen); + Bool (* query_extension) (Display *dpy, int *error, int *event); + MetaDisplay *display = meta_plugin_get_display (META_PLUGIN (shell_plugin)); + MetaX11Display *x11_display = meta_display_get_x11_display (display); + Display *xdisplay; + int screen_number; + const char *glx_extensions; + + /* We will only get swap events if Cogl is using GLX */ + if (cogl_renderer_get_winsys_id (renderer) != COGL_WINSYS_ID_GLX) + return FALSE; + + xdisplay = meta_x11_display_get_xdisplay (x11_display); + + query_extensions_string = + (void *) cogl_get_proc_address ("glXQueryExtensionsString"); + query_extension = + (void *) cogl_get_proc_address ("glXQueryExtension"); + + query_extension (xdisplay, + &shell_plugin->glx_error_base, + &shell_plugin->glx_event_base); + + screen_number = XDefaultScreen (xdisplay); + glx_extensions = query_extensions_string (xdisplay, screen_number); + + return strstr (glx_extensions, "GLX_INTEL_swap_event") != NULL; +} + +static void +gnome_shell_plugin_start (MetaPlugin *plugin) +{ + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + GError *error = NULL; + uint8_t status; + GjsContext *gjs_context; + ClutterBackend *backend; + + backend = clutter_get_default_backend (); + shell_plugin->cogl_context = clutter_backend_get_cogl_context (backend); + + shell_plugin->have_swap_event = + gnome_shell_plugin_has_swap_event (shell_plugin); + + shell_perf_log_define_event (shell_perf_log_get_default (), + "glx.swapComplete", + "GL buffer swap complete event received (with timestamp of completion)", + "x"); + + shell_plugin->global = shell_global_get (); + _shell_global_set_plugin (shell_plugin->global, META_PLUGIN (shell_plugin)); + + gjs_context = _shell_global_get_gjs_context (shell_plugin->global); + + if (!gjs_context_eval_module_file (gjs_context, + "resource:///org/gnome/shell/ui/init.js", + &status, + &error)) + { + g_message ("Execution of main.js threw exception: %s", error->message); + g_error_free (error); + /* We just exit() here, since in a development environment you'll get the + * error in your shell output, and it's way better than a busted WM, + * which typically manifests as a white screen. + * + * In production, we shouldn't crash =) But if we do, we should get + * restarted by the session infrastructure, which is likely going + * to be better than some undefined state. + * + * If there was a generic "hook into bug-buddy for non-C crashes" + * infrastructure, here would be the place to put it. + */ + g_object_unref (gjs_context); + exit (1); + } +} + +static ShellWM * +get_shell_wm (void) +{ + ShellWM *wm; + + g_object_get (shell_global_get (), + "window-manager", &wm, + NULL); + /* drop extra ref added by g_object_get */ + g_object_unref (wm); + + return wm; +} + +static void +gnome_shell_plugin_minimize (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_minimize (get_shell_wm (), + actor); + +} + +static void +gnome_shell_plugin_unminimize (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_unminimize (get_shell_wm (), + actor); + +} + +static void +gnome_shell_plugin_size_changed (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_size_changed (get_shell_wm (), actor); +} + +static void +gnome_shell_plugin_size_change (MetaPlugin *plugin, + MetaWindowActor *actor, + MetaSizeChange which_change, + MetaRectangle *old_frame_rect, + MetaRectangle *old_buffer_rect) +{ + _shell_wm_size_change (get_shell_wm (), actor, which_change, old_frame_rect, old_buffer_rect); +} + +static void +gnome_shell_plugin_map (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_map (get_shell_wm (), + actor); +} + +static void +gnome_shell_plugin_destroy (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_destroy (get_shell_wm (), + actor); +} + +static void +gnome_shell_plugin_switch_workspace (MetaPlugin *plugin, + gint from, + gint to, + MetaMotionDirection direction) +{ + _shell_wm_switch_workspace (get_shell_wm(), from, to, direction); +} + +static void +gnome_shell_plugin_kill_window_effects (MetaPlugin *plugin, + MetaWindowActor *actor) +{ + _shell_wm_kill_window_effects (get_shell_wm(), actor); +} + +static void +gnome_shell_plugin_kill_switch_workspace (MetaPlugin *plugin) +{ + _shell_wm_kill_switch_workspace (get_shell_wm()); +} + +static void +gnome_shell_plugin_show_tile_preview (MetaPlugin *plugin, + MetaWindow *window, + MetaRectangle *tile_rect, + int tile_monitor) +{ + _shell_wm_show_tile_preview (get_shell_wm (), window, tile_rect, tile_monitor); +} + +static void +gnome_shell_plugin_hide_tile_preview (MetaPlugin *plugin) +{ + _shell_wm_hide_tile_preview (get_shell_wm ()); +} + +static void +gnome_shell_plugin_show_window_menu (MetaPlugin *plugin, + MetaWindow *window, + MetaWindowMenuType menu, + int x, + int y) +{ + _shell_wm_show_window_menu (get_shell_wm (), window, menu, x, y); +} + +static void +gnome_shell_plugin_show_window_menu_for_rect (MetaPlugin *plugin, + MetaWindow *window, + MetaWindowMenuType menu, + MetaRectangle *rect) +{ + _shell_wm_show_window_menu_for_rect (get_shell_wm (), window, menu, rect); +} + +static gboolean +gnome_shell_plugin_xevent_filter (MetaPlugin *plugin, + XEvent *xev) +{ +#ifdef GLX_INTEL_swap_event + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + + if (shell_plugin->have_swap_event && + xev->type == (shell_plugin->glx_event_base + GLX_BufferSwapComplete)) + { + GLXBufferSwapComplete *swap_complete_event; + swap_complete_event = (GLXBufferSwapComplete *)xev; + + /* Buggy early versions of the INTEL_swap_event implementation in Mesa + * can send this with a ust of 0. Simplify life for consumers + * by ignoring such events */ + if (swap_complete_event->ust != 0) + { + gboolean frame_timestamps; + g_object_get (shell_plugin->global, + "frame-timestamps", &frame_timestamps, + NULL); + + if (frame_timestamps) + shell_perf_log_event_x (shell_perf_log_get_default (), + "glx.swapComplete", + swap_complete_event->ust); + } + } +#endif + + return FALSE; +} + +static gboolean +gnome_shell_plugin_keybinding_filter (MetaPlugin *plugin, + MetaKeyBinding *binding) +{ + return _shell_wm_filter_keybinding (get_shell_wm (), binding); +} + +static void +gnome_shell_plugin_confirm_display_change (MetaPlugin *plugin) +{ + _shell_wm_confirm_display_change (get_shell_wm ()); +} + +static const MetaPluginInfo * +gnome_shell_plugin_plugin_info (MetaPlugin *plugin) +{ + static const MetaPluginInfo info = { + .name = "GNOME Shell", + .version = "0.1", + .author = "Various", + .license = "GPLv2+", + .description = "Provides GNOME Shell core functionality" + }; + + return &info; +} + +static MetaCloseDialog * +gnome_shell_plugin_create_close_dialog (MetaPlugin *plugin, + MetaWindow *window) +{ + return _shell_wm_create_close_dialog (get_shell_wm (), window); +} + +static MetaInhibitShortcutsDialog * +gnome_shell_plugin_create_inhibit_shortcuts_dialog (MetaPlugin *plugin, + MetaWindow *window) +{ + return _shell_wm_create_inhibit_shortcuts_dialog (get_shell_wm (), window); +} + +static void +gnome_shell_plugin_locate_pointer (MetaPlugin *plugin) +{ + GnomeShellPlugin *shell_plugin = GNOME_SHELL_PLUGIN (plugin); + _shell_global_locate_pointer (shell_plugin->global); +} + +static void +gnome_shell_plugin_class_init (GnomeShellPluginClass *klass) +{ + MetaPluginClass *plugin_class = META_PLUGIN_CLASS (klass); + + plugin_class->start = gnome_shell_plugin_start; + plugin_class->map = gnome_shell_plugin_map; + plugin_class->minimize = gnome_shell_plugin_minimize; + plugin_class->unminimize = gnome_shell_plugin_unminimize; + plugin_class->size_changed = gnome_shell_plugin_size_changed; + plugin_class->size_change = gnome_shell_plugin_size_change; + plugin_class->destroy = gnome_shell_plugin_destroy; + + plugin_class->switch_workspace = gnome_shell_plugin_switch_workspace; + + plugin_class->kill_window_effects = gnome_shell_plugin_kill_window_effects; + plugin_class->kill_switch_workspace = gnome_shell_plugin_kill_switch_workspace; + + plugin_class->show_tile_preview = gnome_shell_plugin_show_tile_preview; + plugin_class->hide_tile_preview = gnome_shell_plugin_hide_tile_preview; + plugin_class->show_window_menu = gnome_shell_plugin_show_window_menu; + plugin_class->show_window_menu_for_rect = gnome_shell_plugin_show_window_menu_for_rect; + + plugin_class->xevent_filter = gnome_shell_plugin_xevent_filter; + plugin_class->keybinding_filter = gnome_shell_plugin_keybinding_filter; + + plugin_class->confirm_display_change = gnome_shell_plugin_confirm_display_change; + + plugin_class->plugin_info = gnome_shell_plugin_plugin_info; + + plugin_class->create_close_dialog = gnome_shell_plugin_create_close_dialog; + plugin_class->create_inhibit_shortcuts_dialog = gnome_shell_plugin_create_inhibit_shortcuts_dialog; + + plugin_class->locate_pointer = gnome_shell_plugin_locate_pointer; +} + +static void +gnome_shell_plugin_init (GnomeShellPlugin *shell_plugin) +{ +} diff --git a/src/gnome-shell-portal-helper.c b/src/gnome-shell-portal-helper.c new file mode 100644 index 0000000..a0bebb2 --- /dev/null +++ b/src/gnome-shell-portal-helper.c @@ -0,0 +1,52 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include +#include + +int +main (int argc, char *argv[]) +{ + const char *search_path[] = { "resource:///org/gnome/shell", NULL }; + GError *error = NULL; + GjsContext *context; + int status; + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + context = g_object_new (GJS_TYPE_CONTEXT, + "search-path", search_path, + NULL); + + if (!gjs_context_define_string_array(context, "ARGV", + argc, (const char**)argv, + &error)) + { + g_message("Failed to define ARGV: %s", error->message); + g_error_free (error); + g_object_unref (context); + + return 1; + } + + + if (!gjs_context_eval (context, + "const Main = imports.portalHelper.main; Main.main(ARGV);", + -1, + "
", + &status, + &error)) + { + g_message ("Execution of main.js threw exception: %s", error->message); + g_error_free (error); + g_object_unref (context); + + return status; + } + + g_object_unref (context); + return 0; +} diff --git a/src/gtkactionmuxer.c b/src/gtkactionmuxer.c new file mode 100644 index 0000000..7e3e86e --- /dev/null +++ b/src/gtkactionmuxer.c @@ -0,0 +1,945 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 licence, 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 . + * + * Author: Ryan Lortie + */ + +#include "config.h" + +#include "gtkactionmuxer.h" + +#include "gtkactionobservable.h" +#include "gtkactionobserver.h" + +#include + +#include + +/** + * SECTION:gtkactionmuxer + * @short_description: Aggregate and monitor several action groups + * + * #GtkActionMuxer is a #GActionGroup and #GtkActionObservable that is + * capable of containing other #GActionGroup instances. + * + * The typical use is aggregating all of the actions applicable to a + * particular context into a single action group, with namespacing. + * + * Consider the case of two action groups -- one containing actions + * applicable to an entire application (such as 'quit') and one + * containing actions applicable to a particular window in the + * application (such as 'fullscreen'). + * + * In this case, each of these action groups could be added to a + * #GtkActionMuxer with the prefixes "app" and "win", respectively. This + * would expose the actions as "app.quit" and "win.fullscreen" on the + * #GActionGroup interface presented by the #GtkActionMuxer. + * + * Activations and state change requests on the #GtkActionMuxer are wired + * through to the underlying action group in the expected way. + * + * This class is typically only used at the site of "consumption" of + * actions (eg: when displaying a menu that contains many actions on + * different objects). + */ + +static void gtk_action_muxer_group_iface_init (GActionGroupInterface *iface); +static void gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface); + +typedef GObjectClass GtkActionMuxerClass; + +struct _GtkActionMuxer +{ + GObject parent_instance; + + GHashTable *observed_actions; + GHashTable *groups; + GHashTable *primary_accels; + GtkActionMuxer *parent; +}; + +G_DEFINE_TYPE_WITH_CODE (GtkActionMuxer, gtk_action_muxer, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (G_TYPE_ACTION_GROUP, gtk_action_muxer_group_iface_init) + G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTION_OBSERVABLE, gtk_action_muxer_observable_iface_init)) + +enum +{ + PROP_0, + PROP_PARENT, + NUM_PROPERTIES +}; + +static GParamSpec *properties[NUM_PROPERTIES]; + +guint accel_signal; + +typedef struct +{ + GtkActionMuxer *muxer; + GSList *watchers; + gchar *fullname; +} Action; + +typedef struct +{ + GtkActionMuxer *muxer; + GActionGroup *group; + gchar *prefix; + gulong handler_ids[4]; +} Group; + +static void +gtk_action_muxer_append_group_actions (gpointer key, + gpointer value, + gpointer user_data) +{ + const gchar *prefix = key; + Group *group = value; + GArray *actions = user_data; + gchar **group_actions; + gchar **action; + + group_actions = g_action_group_list_actions (group->group); + for (action = group_actions; *action; action++) + { + gchar *fullname; + + fullname = g_strconcat (prefix, ".", *action, NULL); + g_array_append_val (actions, fullname); + } + + g_strfreev (group_actions); +} + +static gchar ** +gtk_action_muxer_list_actions (GActionGroup *action_group) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + GArray *actions; + + actions = g_array_new (TRUE, FALSE, sizeof (gchar *)); + + for ( ; muxer != NULL; muxer = muxer->parent) + { + g_hash_table_foreach (muxer->groups, + gtk_action_muxer_append_group_actions, + actions); + } + + return (gchar **)(void *) g_array_free (actions, FALSE); +} + +static Group * +gtk_action_muxer_find_group (GtkActionMuxer *muxer, + const gchar *full_name, + const gchar **action_name) +{ + const gchar *dot; + gchar *prefix; + Group *group; + + dot = strchr (full_name, '.'); + + if (!dot) + return NULL; + + prefix = g_strndup (full_name, dot - full_name); + group = g_hash_table_lookup (muxer->groups, prefix); + g_free (prefix); + + if (action_name) + *action_name = dot + 1; + + return group; +} + +static void +gtk_action_muxer_action_enabled_changed (GtkActionMuxer *muxer, + const gchar *action_name, + gboolean enabled) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_enabled_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, enabled); + g_action_group_action_enabled_changed (G_ACTION_GROUP (muxer), action_name, enabled); +} + +static void +gtk_action_muxer_group_action_enabled_changed (GActionGroup *action_group, + const gchar *action_name, + gboolean enabled, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_enabled_changed (group->muxer, fullname, enabled); + + g_free (fullname); +} + +static void +gtk_action_muxer_parent_action_enabled_changed (GActionGroup *action_group, + const gchar *action_name, + gboolean enabled, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_enabled_changed (muxer, action_name, enabled); +} + +static void +gtk_action_muxer_action_state_changed (GtkActionMuxer *muxer, + const gchar *action_name, + GVariant *state) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_state_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name, state); + g_action_group_action_state_changed (G_ACTION_GROUP (muxer), action_name, state); +} + +static void +gtk_action_muxer_group_action_state_changed (GActionGroup *action_group, + const gchar *action_name, + GVariant *state, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_state_changed (group->muxer, fullname, state); + + g_free (fullname); +} + +static void +gtk_action_muxer_parent_action_state_changed (GActionGroup *action_group, + const gchar *action_name, + GVariant *state, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_state_changed (muxer, action_name, state); +} + +static void +gtk_action_muxer_action_added (GtkActionMuxer *muxer, + const gchar *action_name, + GActionGroup *original_group, + const gchar *orignal_action_name) +{ + const GVariantType *parameter_type; + gboolean enabled; + GVariant *state; + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + + if (action && action->watchers && + g_action_group_query_action (original_group, orignal_action_name, + &enabled, ¶meter_type, NULL, NULL, &state)) + { + GSList *node; + + for (node = action->watchers; node; node = node->next) + gtk_action_observer_action_added (node->data, + GTK_ACTION_OBSERVABLE (muxer), + action_name, parameter_type, enabled, state); + + if (state) + g_variant_unref (state); + } + + g_action_group_action_added (G_ACTION_GROUP (muxer), action_name); +} + +static void +gtk_action_muxer_action_added_to_group (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_added (group->muxer, fullname, action_group, action_name); + + g_free (fullname); +} + +static void +gtk_action_muxer_action_added_to_parent (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_added (muxer, action_name, action_group, action_name); +} + +static void +gtk_action_muxer_action_removed (GtkActionMuxer *muxer, + const gchar *action_name) +{ + Action *action; + GSList *node; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_action_removed (node->data, GTK_ACTION_OBSERVABLE (muxer), action_name); + g_action_group_action_removed (G_ACTION_GROUP (muxer), action_name); +} + +static void +gtk_action_muxer_action_removed_from_group (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + Group *group = user_data; + gchar *fullname; + + fullname = g_strconcat (group->prefix, ".", action_name, NULL); + gtk_action_muxer_action_removed (group->muxer, fullname); + + g_free (fullname); +} + +static void +gtk_action_muxer_action_removed_from_parent (GActionGroup *action_group, + const gchar *action_name, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + gtk_action_muxer_action_removed (muxer, action_name); +} + +static void +gtk_action_muxer_primary_accel_changed (GtkActionMuxer *muxer, + const gchar *action_name, + const gchar *action_and_target) +{ + Action *action; + GSList *node; + + if (!action_name) + action_name = strrchr (action_and_target, '|') + 1; + + action = g_hash_table_lookup (muxer->observed_actions, action_name); + for (node = action ? action->watchers : NULL; node; node = node->next) + gtk_action_observer_primary_accel_changed (node->data, GTK_ACTION_OBSERVABLE (muxer), + action_name, action_and_target); + g_signal_emit (muxer, accel_signal, 0, action_name, action_and_target); +} + +static void +gtk_action_muxer_parent_primary_accel_changed (GtkActionMuxer *parent, + const gchar *action_name, + const gchar *action_and_target, + gpointer user_data) +{ + GtkActionMuxer *muxer = user_data; + + /* If it's in our table then don't let the parent one filter through */ + if (muxer->primary_accels && g_hash_table_lookup (muxer->primary_accels, action_and_target)) + return; + + gtk_action_muxer_primary_accel_changed (muxer, action_name, action_and_target); +} + +static gboolean +gtk_action_muxer_query_action (GActionGroup *action_group, + const gchar *action_name, + gboolean *enabled, + const GVariantType **parameter_type, + const GVariantType **state_type, + GVariant **state_hint, + GVariant **state) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + return g_action_group_query_action (group->group, unprefixed_name, enabled, + parameter_type, state_type, state_hint, state); + + if (muxer->parent) + return g_action_group_query_action (G_ACTION_GROUP (muxer->parent), action_name, + enabled, parameter_type, + state_type, state_hint, state); + + return FALSE; +} + +static GVariant * +get_platform_data (void) +{ + gchar time[32]; + GVariantBuilder *builder; + GVariant *result; + + g_snprintf (time, 32, "_TIME%d", clutter_get_current_event_time ()); + + builder = g_variant_builder_new (G_VARIANT_TYPE ("a{sv}")); + + g_variant_builder_add (builder, "{sv}", "desktop-startup-id", + g_variant_new_string (time)); + + result = g_variant_builder_end (builder); + g_variant_builder_unref (builder); + + return result; +} + +static void +gtk_action_muxer_activate_action (GActionGroup *action_group, + const gchar *action_name, + GVariant *parameter) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + { + if (G_IS_REMOTE_ACTION_GROUP (group->group)) + g_remote_action_group_activate_action_full (G_REMOTE_ACTION_GROUP (group->group), + unprefixed_name, parameter, + get_platform_data ()); + else + g_action_group_activate_action (group->group, unprefixed_name, parameter); + } + else if (muxer->parent) + g_action_group_activate_action (G_ACTION_GROUP (muxer->parent), action_name, parameter); +} + +static void +gtk_action_muxer_change_action_state (GActionGroup *action_group, + const gchar *action_name, + GVariant *state) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (action_group); + Group *group; + const gchar *unprefixed_name; + + group = gtk_action_muxer_find_group (muxer, action_name, &unprefixed_name); + + if (group) + { + if (G_IS_REMOTE_ACTION_GROUP (group->group)) + g_remote_action_group_change_action_state_full (G_REMOTE_ACTION_GROUP (group->group), + unprefixed_name, + state, + get_platform_data ()); + else + g_action_group_change_action_state (group->group, unprefixed_name, state); + } + else if (muxer->parent) + g_action_group_change_action_state (G_ACTION_GROUP (muxer->parent), action_name, state); +} + +static void +gtk_action_muxer_unregister_internal (Action *action, + gpointer observer) +{ + GtkActionMuxer *muxer = action->muxer; + GSList **ptr; + + for (ptr = &action->watchers; *ptr; ptr = &(*ptr)->next) + if ((*ptr)->data == observer) + { + *ptr = g_slist_remove (*ptr, observer); + + if (action->watchers == NULL) + g_hash_table_remove (muxer->observed_actions, action->fullname); + + break; + } +} + +static void +gtk_action_muxer_weak_notify (gpointer data, + GObject *where_the_object_was) +{ + Action *action = data; + + gtk_action_muxer_unregister_internal (action, where_the_object_was); +} + +static void +gtk_action_muxer_register_observer (GtkActionObservable *observable, + const gchar *name, + GtkActionObserver *observer) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable); + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, name); + + if (action == NULL) + { + action = g_new (Action, 1); + action->muxer = muxer; + action->fullname = g_strdup (name); + action->watchers = NULL; + + g_hash_table_insert (muxer->observed_actions, action->fullname, action); + } + + action->watchers = g_slist_prepend (action->watchers, observer); + g_object_weak_ref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action); +} + +static void +gtk_action_muxer_unregister_observer (GtkActionObservable *observable, + const gchar *name, + GtkActionObserver *observer) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (observable); + Action *action; + + action = g_hash_table_lookup (muxer->observed_actions, name); + g_object_weak_unref (G_OBJECT (observer), gtk_action_muxer_weak_notify, action); + gtk_action_muxer_unregister_internal (action, observer); +} + +static void +gtk_action_muxer_free_group (gpointer data) +{ + Group *group = data; + gint i; + + /* 'for loop' or 'four loop'? */ + for (i = 0; i < 4; i++) + g_clear_signal_handler (&group->handler_ids[i], group->group); + + g_object_unref (group->group); + g_free (group->prefix); + + g_free (group); +} + +static void +gtk_action_muxer_free_action (gpointer data) +{ + Action *action = data; + GSList *it; + + for (it = action->watchers; it; it = it->next) + g_object_weak_unref (G_OBJECT (it->data), gtk_action_muxer_weak_notify, action); + + g_slist_free (action->watchers); + g_free (action->fullname); + + g_free (action); +} + +static void +gtk_action_muxer_finalize (GObject *object) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + g_assert_cmpint (g_hash_table_size (muxer->observed_actions), ==, 0); + g_hash_table_unref (muxer->observed_actions); + g_hash_table_unref (muxer->groups); + + G_OBJECT_CLASS (gtk_action_muxer_parent_class) + ->finalize (object); +} + +static void +gtk_action_muxer_dispose (GObject *object) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + if (muxer->parent) + { + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_primary_accel_changed, muxer); + + g_clear_object (&muxer->parent); + } + + g_hash_table_remove_all (muxer->observed_actions); + + G_OBJECT_CLASS (gtk_action_muxer_parent_class) + ->dispose (object); +} + +static void +gtk_action_muxer_get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + switch (property_id) + { + case PROP_PARENT: + g_value_set_object (value, gtk_action_muxer_get_parent (muxer)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtk_action_muxer_set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + GtkActionMuxer *muxer = GTK_ACTION_MUXER (object); + + switch (property_id) + { + case PROP_PARENT: + gtk_action_muxer_set_parent (muxer, g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + } +} + +static void +gtk_action_muxer_init (GtkActionMuxer *muxer) +{ + muxer->observed_actions = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_action); + muxer->groups = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, gtk_action_muxer_free_group); +} + +static void +gtk_action_muxer_observable_iface_init (GtkActionObservableInterface *iface) +{ + iface->register_observer = gtk_action_muxer_register_observer; + iface->unregister_observer = gtk_action_muxer_unregister_observer; +} + +static void +gtk_action_muxer_group_iface_init (GActionGroupInterface *iface) +{ + iface->list_actions = gtk_action_muxer_list_actions; + iface->query_action = gtk_action_muxer_query_action; + iface->activate_action = gtk_action_muxer_activate_action; + iface->change_action_state = gtk_action_muxer_change_action_state; +} + +static void +gtk_action_muxer_class_init (GObjectClass *class) +{ + class->get_property = gtk_action_muxer_get_property; + class->set_property = gtk_action_muxer_set_property; + class->finalize = gtk_action_muxer_finalize; + class->dispose = gtk_action_muxer_dispose; + + accel_signal = g_signal_new ("primary-accel-changed", GTK_TYPE_ACTION_MUXER, G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, G_TYPE_NONE, 2, G_TYPE_STRING, G_TYPE_STRING); + + properties[PROP_PARENT] = g_param_spec_object ("parent", "Parent", + "The parent muxer", + GTK_TYPE_ACTION_MUXER, + G_PARAM_READWRITE | + G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (class, NUM_PROPERTIES, properties); +} + +/** + * gtk_action_muxer_insert: + * @muxer: a #GtkActionMuxer + * @prefix: the prefix string for the action group + * @action_group: a #GActionGroup + * + * Adds the actions in @action_group to the list of actions provided by + * @muxer. @prefix is prefixed to each action name, such that for each + * action x in @action_group, there is an equivalent + * action @prefix.x in @muxer. + * + * For example, if @prefix is "app" and @action_group + * contains an action called "quit", then @muxer will + * now contain an action called "app.quit". + * + * If any #GtkActionObservers are registered for actions in the group, + * "action_added" notifications will be emitted, as appropriate. + * + * @prefix must not contain a dot ('.'). + */ +void +gtk_action_muxer_insert (GtkActionMuxer *muxer, + const gchar *prefix, + GActionGroup *action_group) +{ + gchar **actions; + Group *group; + gint i; + + /* TODO: diff instead of ripout and replace */ + gtk_action_muxer_remove (muxer, prefix); + + group = g_new (Group, 1); + group->muxer = muxer; + group->group = g_object_ref (action_group); + group->prefix = g_strdup (prefix); + + g_hash_table_insert (muxer->groups, group->prefix, group); + + actions = g_action_group_list_actions (group->group); + for (i = 0; actions[i]; i++) + gtk_action_muxer_action_added_to_group (group->group, actions[i], group); + g_strfreev (actions); + + group->handler_ids[0] = g_signal_connect (group->group, "action-added", + G_CALLBACK (gtk_action_muxer_action_added_to_group), group); + group->handler_ids[1] = g_signal_connect (group->group, "action-removed", + G_CALLBACK (gtk_action_muxer_action_removed_from_group), group); + group->handler_ids[2] = g_signal_connect (group->group, "action-enabled-changed", + G_CALLBACK (gtk_action_muxer_group_action_enabled_changed), group); + group->handler_ids[3] = g_signal_connect (group->group, "action-state-changed", + G_CALLBACK (gtk_action_muxer_group_action_state_changed), group); +} + +/** + * gtk_action_muxer_remove: + * @muxer: a #GtkActionMuxer + * @prefix: the prefix of the action group to remove + * + * Removes a #GActionGroup from the #GtkActionMuxer. + * + * If any #GtkActionObservers are registered for actions in the group, + * "action_removed" notifications will be emitted, as appropriate. + */ +void +gtk_action_muxer_remove (GtkActionMuxer *muxer, + const gchar *prefix) +{ + Group *group; + + group = g_hash_table_lookup (muxer->groups, prefix); + + if (group != NULL) + { + gchar **actions; + gint i; + + g_hash_table_steal (muxer->groups, prefix); + + actions = g_action_group_list_actions (group->group); + for (i = 0; actions[i]; i++) + gtk_action_muxer_action_removed_from_group (group->group, actions[i], group); + g_strfreev (actions); + + gtk_action_muxer_free_group (group); + } +} + +/** + * gtk_action_muxer_new: + * + * Creates a new #GtkActionMuxer. + */ +GtkActionMuxer * +gtk_action_muxer_new (void) +{ + return g_object_new (GTK_TYPE_ACTION_MUXER, NULL); +} + +/** + * gtk_action_muxer_get_parent: + * @muxer: a #GtkActionMuxer + * + * Returns: (transfer none): the parent of @muxer, or NULL. + */ +GtkActionMuxer * +gtk_action_muxer_get_parent (GtkActionMuxer *muxer) +{ + g_return_val_if_fail (GTK_IS_ACTION_MUXER (muxer), NULL); + + return muxer->parent; +} + +static void +emit_changed_accels (GtkActionMuxer *muxer, + GtkActionMuxer *parent) +{ + while (parent) + { + if (parent->primary_accels) + { + GHashTableIter iter; + gpointer key; + + g_hash_table_iter_init (&iter, parent->primary_accels); + while (g_hash_table_iter_next (&iter, &key, NULL)) + gtk_action_muxer_primary_accel_changed (muxer, NULL, key); + } + + parent = parent->parent; + } +} + +/** + * gtk_action_muxer_set_parent: + * @muxer: a #GtkActionMuxer + * @parent: (nullable): the new parent #GtkActionMuxer + * + * Sets the parent of @muxer to @parent. + */ +void +gtk_action_muxer_set_parent (GtkActionMuxer *muxer, + GtkActionMuxer *parent) +{ + g_return_if_fail (GTK_IS_ACTION_MUXER (muxer)); + g_return_if_fail (parent == NULL || GTK_IS_ACTION_MUXER (parent)); + + if (muxer->parent == parent) + return; + + if (muxer->parent != NULL) + { + gchar **actions; + gchar **it; + + actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent)); + for (it = actions; *it; it++) + gtk_action_muxer_action_removed (muxer, *it); + g_strfreev (actions); + + emit_changed_accels (muxer, muxer->parent); + + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_added_to_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_action_removed_from_parent, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_enabled_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_action_state_changed, muxer); + g_signal_handlers_disconnect_by_func (muxer->parent, gtk_action_muxer_parent_primary_accel_changed, muxer); + + g_object_unref (muxer->parent); + } + + muxer->parent = parent; + + if (muxer->parent != NULL) + { + gchar **actions; + gchar **it; + + g_object_ref (muxer->parent); + + actions = g_action_group_list_actions (G_ACTION_GROUP (muxer->parent)); + for (it = actions; *it; it++) + gtk_action_muxer_action_added (muxer, *it, G_ACTION_GROUP (muxer->parent), *it); + g_strfreev (actions); + + emit_changed_accels (muxer, muxer->parent); + + g_signal_connect (muxer->parent, "action-added", + G_CALLBACK (gtk_action_muxer_action_added_to_parent), muxer); + g_signal_connect (muxer->parent, "action-removed", + G_CALLBACK (gtk_action_muxer_action_removed_from_parent), muxer); + g_signal_connect (muxer->parent, "action-enabled-changed", + G_CALLBACK (gtk_action_muxer_parent_action_enabled_changed), muxer); + g_signal_connect (muxer->parent, "action-state-changed", + G_CALLBACK (gtk_action_muxer_parent_action_state_changed), muxer); + g_signal_connect (muxer->parent, "primary-accel-changed", + G_CALLBACK (gtk_action_muxer_parent_primary_accel_changed), muxer); + } + + g_object_notify_by_pspec (G_OBJECT (muxer), properties[PROP_PARENT]); +} + +void +gtk_action_muxer_set_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target, + const gchar *primary_accel) +{ + if (!muxer->primary_accels) + muxer->primary_accels = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + if (primary_accel) + g_hash_table_insert (muxer->primary_accels, g_strdup (action_and_target), g_strdup (primary_accel)); + else + g_hash_table_remove (muxer->primary_accels, action_and_target); + + gtk_action_muxer_primary_accel_changed (muxer, NULL, action_and_target); +} + +const gchar * +gtk_action_muxer_get_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target) +{ + if (muxer->primary_accels) + { + const gchar *primary_accel; + + primary_accel = g_hash_table_lookup (muxer->primary_accels, action_and_target); + + if (primary_accel) + return primary_accel; + } + + if (!muxer->parent) + return NULL; + + return gtk_action_muxer_get_primary_accel (muxer->parent, action_and_target); +} + +gchar * +gtk_print_action_and_target (const gchar *action_namespace, + const gchar *action_name, + GVariant *target) +{ + GString *result; + + g_return_val_if_fail (strchr (action_name, '|') == NULL, NULL); + g_return_val_if_fail (action_namespace == NULL || strchr (action_namespace, '|') == NULL, NULL); + + result = g_string_new (NULL); + + if (target) + g_variant_print_string (target, result, TRUE); + g_string_append_c (result, '|'); + + if (action_namespace) + { + g_string_append (result, action_namespace); + g_string_append_c (result, '.'); + } + + g_string_append (result, action_name); + + return g_string_free (result, FALSE); +} diff --git a/src/gtkactionmuxer.h b/src/gtkactionmuxer.h new file mode 100644 index 0000000..d71abf4 --- /dev/null +++ b/src/gtkactionmuxer.h @@ -0,0 +1,64 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 licence, 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 . + * + * Author: Ryan Lortie + */ + +#ifndef __GTK_ACTION_MUXER_H__ +#define __GTK_ACTION_MUXER_H__ + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_MUXER (gtk_action_muxer_get_type ()) +#define GTK_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_MUXER, GtkActionMuxer)) +#define GTK_IS_ACTION_MUXER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_MUXER)) + +typedef struct _GtkActionMuxer GtkActionMuxer; + +GType gtk_action_muxer_get_type (void); +GtkActionMuxer * gtk_action_muxer_new (void); + +void gtk_action_muxer_insert (GtkActionMuxer *muxer, + const gchar *prefix, + GActionGroup *action_group); + +void gtk_action_muxer_remove (GtkActionMuxer *muxer, + const gchar *prefix); + +GtkActionMuxer * gtk_action_muxer_get_parent (GtkActionMuxer *muxer); + +void gtk_action_muxer_set_parent (GtkActionMuxer *muxer, + GtkActionMuxer *parent); + +void gtk_action_muxer_set_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target, + const gchar *primary_accel); + +const gchar * gtk_action_muxer_get_primary_accel (GtkActionMuxer *muxer, + const gchar *action_and_target); + +/* No better place for this... */ +gchar * gtk_print_action_and_target (const gchar *action_namespace, + const gchar *action_name, + GVariant *target); + +G_END_DECLS + +#endif /* __GTK_ACTION_MUXER_H__ */ diff --git a/src/gtkactionobservable.c b/src/gtkactionobservable.c new file mode 100644 index 0000000..ab90df2 --- /dev/null +++ b/src/gtkactionobservable.c @@ -0,0 +1,78 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 + * licence 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: Ryan Lortie + */ + +#include "config.h" + +#include "gtkactionobservable.h" + +G_DEFINE_INTERFACE (GtkActionObservable, gtk_action_observable, G_TYPE_OBJECT) + +/* + * SECTION:gtkactionobserable + * @short_description: an interface implemented by objects that report + * changes to actions + */ + +void +gtk_action_observable_default_init (GtkActionObservableInterface *iface) +{ +} + +/** + * gtk_action_observable_register_observer: + * @observable: a #GtkActionObservable + * @action_name: the name of the action + * @observer: the #GtkActionObserver to which the events will be reported + * + * Registers @observer as being interested in changes to @action_name on + * @observable. + */ +void +gtk_action_observable_register_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable)); + + GTK_ACTION_OBSERVABLE_GET_IFACE (observable) + ->register_observer (observable, action_name, observer); +} + +/** + * gtk_action_observable_unregister_observer: + * @observable: a #GtkActionObservable + * @action_name: the name of the action + * @observer: the #GtkActionObserver to which the events will be reported + * + * Removes the registration of @observer as being interested in changes + * to @action_name on @observable. + * + * If the observer was registered multiple times, it must be + * unregistered an equal number of times. + */ +void +gtk_action_observable_unregister_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVABLE (observable)); + + GTK_ACTION_OBSERVABLE_GET_IFACE (observable) + ->unregister_observer (observable, action_name, observer); +} diff --git a/src/gtkactionobservable.h b/src/gtkactionobservable.h new file mode 100644 index 0000000..aa1514b --- /dev/null +++ b/src/gtkactionobservable.h @@ -0,0 +1,60 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 + * licence 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: Ryan Lortie + */ + +#ifndef __GTK_ACTION_OBSERVABLE_H__ +#define __GTK_ACTION_OBSERVABLE_H__ + +#include "gtkactionobserver.h" + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_OBSERVABLE (gtk_action_observable_get_type ()) +#define GTK_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE, GtkActionObservable)) +#define GTK_IS_ACTION_OBSERVABLE(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE)) +#define GTK_ACTION_OBSERVABLE_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \ + GTK_TYPE_ACTION_OBSERVABLE, \ + GtkActionObservableInterface)) + +typedef struct _GtkActionObservableInterface GtkActionObservableInterface; + +struct _GtkActionObservableInterface +{ + GTypeInterface g_iface; + + void (* register_observer) (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); + void (* unregister_observer) (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); +}; + +GType gtk_action_observable_get_type (void); +void gtk_action_observable_register_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); +void gtk_action_observable_unregister_observer (GtkActionObservable *observable, + const gchar *action_name, + GtkActionObserver *observer); + +G_END_DECLS + +#endif /* __GTK_ACTION_OBSERVABLE_H__ */ diff --git a/src/gtkactionobserver.c b/src/gtkactionobserver.c new file mode 100644 index 0000000..3287106 --- /dev/null +++ b/src/gtkactionobserver.c @@ -0,0 +1,189 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 + * licence 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: Ryan Lortie + */ + +#include "config.h" + +#include "gtkactionobserver.h" + +G_DEFINE_INTERFACE (GtkActionObserver, gtk_action_observer, G_TYPE_OBJECT) + +/** + * SECTION:gtkactionobserver + * @short_description: an interface implemented by objects that are + * interested in monitoring actions for changes + * + * GtkActionObserver is a simple interface allowing objects that wish to + * be notified of changes to actions to be notified of those changes. + * + * It is also possible to monitor changes to action groups using + * #GObject signals, but there are a number of reasons that this + * approach could become problematic: + * + * - there are four separate signals that must be manually connected + * and disconnected + * + * - when a large number of different observers wish to monitor a + * (usually disjoint) set of actions within the same action group, + * there is only one way to avoid having all notifications delivered + * to all observers: signal detail. In order to use signal detail, + * each action name must be quarked, which is not always practical. + * + * - even if quarking is acceptable, #GObject signal details are + * implemented by scanning a linked list, so there is no real + * decrease in complexity + */ + +void +gtk_action_observer_default_init (GtkActionObserverInterface *class) +{ +} + +/** + * gtk_action_observer_action_added: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @enabled: %TRUE if the action is now enabled + * @parameter_type: the parameter type for action invocations, or %NULL + * if no parameter is required + * @state: the current state of the action, or %NULL if the action is + * stateless + * + * This function is called when an action that the observer is + * registered to receive events for is added. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_added (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_added (observer, observable, action_name, parameter_type, enabled, state); +} + +/** + * gtk_action_observer_action_enabled_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @enabled: %TRUE if the action is now enabled + * + * This function is called when an action that the observer is + * registered to receive events for becomes enabled or disabled. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_enabled_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_enabled_changed (observer, observable, action_name, enabled); +} + +/** + * gtk_action_observer_action_state_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @state: the new state of the action + * + * This function is called when an action that the observer is + * registered to receive events for changes to its state. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_state_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_state_changed (observer, observable, action_name, state); +} + +/** + * gtk_action_observer_action_removed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * + * This function is called when an action that the observer is + * registered to receive events for is removed. + * + * This function should only be called by objects with which the + * observer has explicitly registered itself to receive events. + */ +void +gtk_action_observer_action_removed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name) +{ + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + GTK_ACTION_OBSERVER_GET_IFACE (observer) + ->action_removed (observer, observable, action_name); +} + +/** + * gtk_action_observer_primary_accel_changed: + * @observer: a #GtkActionObserver + * @observable: the source of the event + * @action_name: the name of the action + * @action_and_target: detailed action of the changed accel, in "action and target" format + * + * This function is called when an action that the observer is + * registered to receive events for has one of its accelerators changed. + * + * Accelerator changes are reported for all targets associated with the + * action. The @action_and_target string should be used to check if the + * reported target is the one that the observer is interested in. + */ +void +gtk_action_observer_primary_accel_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target) +{ + GtkActionObserverInterface *iface; + + g_return_if_fail (GTK_IS_ACTION_OBSERVER (observer)); + + iface = GTK_ACTION_OBSERVER_GET_IFACE (observer); + + if (iface->primary_accel_changed) + iface->primary_accel_changed (observer, observable, action_name, action_and_target); +} diff --git a/src/gtkactionobserver.h b/src/gtkactionobserver.h new file mode 100644 index 0000000..a4e9659 --- /dev/null +++ b/src/gtkactionobserver.h @@ -0,0 +1,91 @@ +/* + * Copyright © 2011 Canonical Limited + * + * 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 + * licence 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: Ryan Lortie + */ + +#ifndef __GTK_ACTION_OBSERVER_H__ +#define __GTK_ACTION_OBSERVER_H__ + +#include + +G_BEGIN_DECLS + +#define GTK_TYPE_ACTION_OBSERVER (gtk_action_observer_get_type ()) +#define GTK_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_CAST ((inst), \ + GTK_TYPE_ACTION_OBSERVER, GtkActionObserver)) +#define GTK_IS_ACTION_OBSERVER(inst) (G_TYPE_CHECK_INSTANCE_TYPE ((inst), \ + GTK_TYPE_ACTION_OBSERVER)) +#define GTK_ACTION_OBSERVER_GET_IFACE(inst) (G_TYPE_INSTANCE_GET_INTERFACE ((inst), \ + GTK_TYPE_ACTION_OBSERVER, GtkActionObserverInterface)) + +typedef struct _GtkActionObserverInterface GtkActionObserverInterface; +typedef struct _GtkActionObservable GtkActionObservable; +typedef struct _GtkActionObserver GtkActionObserver; + +struct _GtkActionObserverInterface +{ + GTypeInterface g_iface; + + void (* action_added) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state); + void (* action_enabled_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled); + void (* action_state_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state); + void (* action_removed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name); + void (* primary_accel_changed) (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target); +}; + +GType gtk_action_observer_get_type (void); +void gtk_action_observer_action_added (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const GVariantType *parameter_type, + gboolean enabled, + GVariant *state); +void gtk_action_observer_action_enabled_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + gboolean enabled); +void gtk_action_observer_action_state_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + GVariant *state); +void gtk_action_observer_action_removed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name); +void gtk_action_observer_primary_accel_changed (GtkActionObserver *observer, + GtkActionObservable *observable, + const gchar *action_name, + const gchar *action_and_target); + +G_END_DECLS + +#endif /* __GTK_ACTION_OBSERVER_H__ */ diff --git a/src/hotplug-sniffer/hotplug-mimetypes.h b/src/hotplug-sniffer/hotplug-mimetypes.h new file mode 100644 index 0000000..b034020 --- /dev/null +++ b/src/hotplug-sniffer/hotplug-mimetypes.h @@ -0,0 +1,141 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#ifndef __HOTPLUG_MIMETYPES_H__ +#define __HOTPLUG_MIMETYPES_H__ + +#include + +G_GNUC_UNUSED static const gchar *docs_mimetypes[] = { + "application/vnd.oasis.opendocument.text", + "application/vnd.oasis.opendocument.presentation", + "application/vnd.oasis.opendocument.spreadsheet", + "application/msword", + "application/vnd.ms-excel", + "application/vnd.ms-powerpoint", + "application/rtf", + "application/pdf", + "application/x-bzpdf", + "application/x-gzpdf", + "application/x-xzpdf", + "application/postscript", + "application/x-bzpostscript", + "application/x-gzpostscript", + "image/x-eps", + "image/x-bzeps", + "image/x-gzeps", + "application/x-dvi", + "application/x-bzdvi", + "application/x-gzdvi", + "image/vnd.djvu", + "application/x-cbr", + "application/x-cbz", + "application/x-cb7", + "application/x-cbt", + NULL +}; + +G_GNUC_UNUSED static const gchar *video_mimetypes[] = { + "application/mxf", + "application/ogg", + "application/ram", + "application/sdp", + "application/vnd.ms-wpl", + "application/vnd.rn-realmedia", + "application/x-extension-m4a", + "application/x-extension-mp4", + "application/x-flash-video", + "application/x-matroska", + "application/x-netshow-channel", + "application/x-ogg", + "application/x-quicktimeplayer", + "application/x-shorten", + "image/vnd.rn-realpix", + "image/x-pict", + "misc/ultravox", + "text/x-google-video-pointer", + "video/3gpp", + "video/dv", + "video/fli", + "video/flv", + "video/mp2t", + "video/mp4", + "video/mp4v-es", + "video/mpeg", + "video/msvideo", + "video/ogg", + "video/quicktime", + "video/vivo", + "video/vnd.divx", + "video/vnd.rn-realvideo", + "video/vnd.vivo", + "video/webm", + "video/x-anim", + "video/x-avi", + "video/x-flc", + "video/x-fli", + "video/x-flic", + "video/x-flv", + "video/x-m4v", + "video/x-matroska", + "video/x-mpeg", + "video/x-ms-asf", + "video/x-ms-asx", + "video/x-msvideo", + "video/x-ms-wm", + "video/x-ms-wmv", + "video/x-ms-wmx", + "video/x-ms-wvx", + "video/x-nsv", + "video/x-ogm+ogg", + "video/x-theora+ogg", + "video/x-totem-stream", + NULL +}; + +G_GNUC_UNUSED static const gchar *audio_mimetypes[] = { + "audio/3gpp", + "audio/ac3", + "audio/AMR", + "audio/AMR-WB", + "audio/basic", + "audio/flac", + "audio/midi", + "audio/mp2", + "audio/mp4", + "audio/mpeg", + "audio/ogg", + "audio/prs.sid", + "audio/vnd.rn-realaudio", + "audio/x-aiff", + "audio/x-ape", + "audio/x-flac", + "audio/x-gsm", + "audio/x-it", + "audio/x-m4a", + "audio/x-matroska", + "audio/x-mod", + "audio/x-mp3", + "audio/x-mpeg", + "audio/x-ms-asf", + "audio/x-ms-asx", + "audio/x-ms-wax", + "audio/x-ms-wma", + "audio/x-musepack", + "audio/x-pn-aiff", + "audio/x-pn-au", + "audio/x-pn-wav", + "audio/x-pn-windows-acm", + "audio/x-realaudio", + "audio/x-real-audio", + "audio/x-sbc", + "audio/x-speex", + "audio/x-tta", + "audio/x-wav", + "audio/x-wavpack", + "audio/x-vorbis", + "audio/x-vorbis+ogg", + "audio/x-xm", + NULL +}; + +#endif /* __HOTPLUG_MIMETYPES_H__ */ diff --git a/src/hotplug-sniffer/hotplug-sniffer.c b/src/hotplug-sniffer/hotplug-sniffer.c new file mode 100644 index 0000000..4b70ec7 --- /dev/null +++ b/src/hotplug-sniffer/hotplug-sniffer.c @@ -0,0 +1,298 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * Authors: David Zeuthen + * Cosimo Cecchi + * + */ + +#include "shell-mime-sniffer.h" +#include "hotplug-mimetypes.h" + +/* Set the environment variable HOTPLUG_SNIFFER_DEBUG to show debug */ +static void print_debug (const gchar *str, ...); + +#define BUS_NAME "org.gnome.Shell.HotplugSniffer" +#define AUTOQUIT_TIMEOUT 5 + +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + ""; + +static GDBusNodeInfo *introspection_data = NULL; +static GMainLoop *loop = NULL; +static guint autoquit_id = 0; + +static gboolean +autoquit_timeout_cb (gpointer _unused) +{ + print_debug ("Timeout reached, quitting..."); + + autoquit_id = 0; + g_main_loop_quit (loop); + + return FALSE; +} + +static void +ensure_autoquit_off (void) +{ + if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL) + return; + + g_clear_handle_id (&autoquit_id, g_source_remove); +} + +static void +ensure_autoquit_on (void) +{ + if (g_getenv ("HOTPLUG_SNIFFER_PERSIST") != NULL) + return; + + autoquit_id = + g_timeout_add_seconds (AUTOQUIT_TIMEOUT, + autoquit_timeout_cb, NULL); + g_source_set_name_by_id (autoquit_id, "[gnome-shell] autoquit_timeout_cb"); +} + +typedef struct { + GVariant *parameters; + GDBusMethodInvocation *invocation; +} InvocationData; + +static InvocationData * +invocation_data_new (GVariant *params, + GDBusMethodInvocation *invocation) +{ + InvocationData *ret; + + ret = g_new0 (InvocationData, 1); + ret->parameters = g_variant_ref (params); + ret->invocation = g_object_ref (invocation); + + return ret; +} + +static void +invocation_data_free (InvocationData *data) +{ + g_variant_unref (data->parameters); + g_clear_object (&data->invocation); + + g_free (data); +} + +static void +sniff_async_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + InvocationData *data = user_data; + gchar **types; + GError *error = NULL; + + types = shell_mime_sniffer_sniff_finish (SHELL_MIME_SNIFFER (source), + res, &error); + + if (error != NULL) + { + g_dbus_method_invocation_return_gerror (data->invocation, error); + g_error_free (error); + goto out; + } + + g_dbus_method_invocation_return_value (data->invocation, + g_variant_new ("(^as)", types)); + g_strfreev (types); + + out: + invocation_data_free (data); + ensure_autoquit_on (); +} + +static void +handle_sniff_uri (InvocationData *data) +{ + ShellMimeSniffer *sniffer; + const gchar *uri; + GFile *file; + + ensure_autoquit_off (); + + g_variant_get (data->parameters, + "(&s)", &uri, + NULL); + file = g_file_new_for_uri (uri); + + print_debug ("Initiating sniff for uri %s", uri); + + sniffer = shell_mime_sniffer_new (file); + shell_mime_sniffer_sniff_async (sniffer, + sniff_async_ready_cb, + data); + + g_object_unref (sniffer); + g_object_unref (file); +} + +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) +{ + InvocationData *data; + + data = invocation_data_new (parameters, invocation); + + if (g_strcmp0 (method_name, "SniffURI") == 0) + handle_sniff_uri (data); + else + g_assert_not_reached (); +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, /* get_property */ + NULL, /* set_property */ +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GError *error = NULL; + + print_debug ("Connected to the session bus: %s", name); + + g_dbus_connection_register_object (connection, + "/org/gnome/Shell/HotplugSniffer", + introspection_data->interfaces[0], + &interface_vtable, + NULL, + NULL, + &error); + + if (error != NULL) + { + g_printerr ("Error exporting object on the session bus: %s", + error->message); + g_error_free (error); + + _exit(1); + } + + print_debug ("Object exported on the session bus"); +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Lost bus name: %s, exiting", name); + + g_main_loop_quit (loop); +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + print_debug ("Acquired bus name: %s", name); +} + +int +main (int argc, + char **argv) +{ + guint name_owner_id; + + introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + g_assert (introspection_data != NULL); + + ensure_autoquit_on (); + loop = g_main_loop_new (NULL, FALSE); + + name_owner_id = g_bus_own_name (G_BUS_TYPE_SESSION, + BUS_NAME, 0, + on_bus_acquired, + on_name_acquired, + on_name_lost, + NULL, + NULL); + + g_main_loop_run (loop); + + if (name_owner_id != 0) + g_bus_unown_name (name_owner_id); + + if (loop != NULL) + g_main_loop_unref (loop); + + return 0; +} + +/* ---------------------------------------------------------------------------------------------------- */ + +static void __attribute__((format(printf, 1, 0))) +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 ("HOTPLUG_SNIFFER_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 ("gnome-shell-hotplug-sniffer[%d]: %s.%03d: %s\n", + pid, timestamp, g_date_time_get_microsecond (now), s); + out: + ; +} + diff --git a/src/hotplug-sniffer/meson.build b/src/hotplug-sniffer/meson.build new file mode 100644 index 0000000..4a777e5 --- /dev/null +++ b/src/hotplug-sniffer/meson.build @@ -0,0 +1,22 @@ +hotplug_sources = [ + 'hotplug-mimetypes.h', + 'shell-mime-sniffer.h', + 'shell-mime-sniffer.c', + 'hotplug-sniffer.c' +] + +executable('gnome-shell-hotplug-sniffer', hotplug_sources, + dependencies: [gio_dep, gdk_pixbuf_dep], + include_directories: include_directories('../..'), + install_dir: libexecdir, + install: true +) + +service_file = 'org.gnome.Shell.HotplugSniffer.service' + +configure_file( + input: service_file + '.in', + output: service_file, + configuration: service_data, + install_dir: servicedir +) diff --git a/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in b/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in new file mode 100644 index 0000000..b14cea9 --- /dev/null +++ b/src/hotplug-sniffer/org.gnome.Shell.HotplugSniffer.service.in @@ -0,0 +1,3 @@ +[D-BUS Service] +Name=org.gnome.Shell.HotplugSniffer +Exec=@libexecdir@/gnome-shell-hotplug-sniffer diff --git a/src/hotplug-sniffer/shell-mime-sniffer.c b/src/hotplug-sniffer/shell-mime-sniffer.c new file mode 100644 index 0000000..7a1c1fe --- /dev/null +++ b/src/hotplug-sniffer/shell-mime-sniffer.c @@ -0,0 +1,590 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 1999, 2000, 2001 Eazel, Inc. + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * Author: Cosimo Cecchi + * + * The code for crawling the directory hierarchy is based on + * nautilus/libnautilus-private/nautilus-directory-async.c, with + * the following copyright and author: + * + * Copyright (C) 1999, 2000, 2001 Eazel, Inc. + * Author: Darin Adler + * + */ + +#include "shell-mime-sniffer.h" +#include "hotplug-mimetypes.h" + +#include + +#include + +#define LOADER_ATTRS \ + G_FILE_ATTRIBUTE_STANDARD_TYPE "," \ + G_FILE_ATTRIBUTE_STANDARD_NAME "," \ + G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE "," + +#define WATCHDOG_TIMEOUT 1500 +#define DIRECTORY_LOAD_ITEMS_PER_CALLBACK 100 +#define HIGH_SCORE_RATIO 0.10 + +enum { + PROP_FILE = 1, + NUM_PROPERTIES +}; + +static GHashTable *image_type_table = NULL; +static GHashTable *audio_type_table = NULL; +static GHashTable *video_type_table = NULL; +static GHashTable *docs_type_table = NULL; + +static GParamSpec *properties[NUM_PROPERTIES] = { NULL, }; + +typedef struct { + ShellMimeSniffer *self; + + GFile *file; + GFileEnumerator *enumerator; + GList *deep_count_subdirectories; + + gint audio_count; + gint image_count; + gint document_count; + gint video_count; + + gint total_items; +} DeepCountState; + +typedef struct _ShellMimeSnifferPrivate ShellMimeSnifferPrivate; + +struct _ShellMimeSniffer +{ + GObject parent_instance; + + ShellMimeSnifferPrivate *priv; +}; + +struct _ShellMimeSnifferPrivate { + GFile *file; + + GCancellable *cancellable; + guint watchdog_id; + + GTask *task; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellMimeSniffer, shell_mime_sniffer, G_TYPE_OBJECT); + +static void deep_count_load (DeepCountState *state, + GFile *file); + +static void +init_mimetypes (void) +{ + static gsize once_init = 0; + + if (g_once_init_enter (&once_init)) + { + GSList *formats, *l; + GdkPixbufFormat *format; + gchar **types; + gint idx; + + image_type_table = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); + video_type_table = g_hash_table_new (g_str_hash, g_str_equal); + audio_type_table = g_hash_table_new (g_str_hash, g_str_equal); + docs_type_table = g_hash_table_new (g_str_hash, g_str_equal); + + formats = gdk_pixbuf_get_formats (); + + for (l = formats; l != NULL; l = l->next) + { + format = l->data; + types = gdk_pixbuf_format_get_mime_types (format); + + for (idx = 0; types[idx] != NULL; idx++) + g_hash_table_insert (image_type_table, g_strdup (types[idx]), GINT_TO_POINTER (1)); + + g_strfreev (types); + } + + g_slist_free (formats); + + for (idx = 0; audio_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (audio_type_table, (gpointer) audio_mimetypes[idx], GINT_TO_POINTER (1)); + + for (idx = 0; video_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (video_type_table, (gpointer) video_mimetypes[idx], GINT_TO_POINTER (1)); + + for (idx = 0; docs_mimetypes[idx] != NULL; idx++) + g_hash_table_insert (docs_type_table, (gpointer) docs_mimetypes[idx], GINT_TO_POINTER (1)); + + g_once_init_leave (&once_init, 1); + } +} + +static void +add_content_type_to_cache (DeepCountState *state, + const gchar *content_type) +{ + gboolean matched = TRUE; + + if (g_hash_table_lookup (image_type_table, content_type)) + state->image_count++; + else if (g_hash_table_lookup (video_type_table, content_type)) + state->video_count++; + else if (g_hash_table_lookup (docs_type_table, content_type)) + state->document_count++; + else if (g_hash_table_lookup (audio_type_table, content_type)) + state->audio_count++; + else + matched = FALSE; + + if (matched) + state->total_items++; +} + +typedef struct { + const gchar *type; + gdouble ratio; +} SniffedResult; + +static gint +results_cmp_func (gconstpointer a, + gconstpointer b) +{ + const SniffedResult *sniffed_a = a; + const SniffedResult *sniffed_b = b; + + if (sniffed_a->ratio < sniffed_b->ratio) + return 1; + + if (sniffed_a->ratio > sniffed_b->ratio) + return -1; + + return 0; +} + +static void +prepare_async_result (DeepCountState *state) +{ + ShellMimeSniffer *self = state->self; + GArray *results; + GPtrArray *sniffed_mime; + SniffedResult result; + char **mimes; + + sniffed_mime = g_ptr_array_new (); + results = g_array_new (TRUE, TRUE, sizeof (SniffedResult)); + + if (state->total_items == 0) + goto out; + + result.type = "x-content/video"; + result.ratio = (gdouble) state->video_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/audio"; + result.ratio = (gdouble) state->audio_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/pictures"; + result.ratio = (gdouble) state->image_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + result.type = "x-content/documents"; + result.ratio = (gdouble) state->document_count / (gdouble) state->total_items; + g_array_append_val (results, result); + + g_array_sort (results, results_cmp_func); + + result = g_array_index (results, SniffedResult, 0); + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + /* if other types score high in ratio, add them, up to three */ + result = g_array_index (results, SniffedResult, 1); + if (result.ratio < HIGH_SCORE_RATIO) + goto out; + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + result = g_array_index (results, SniffedResult, 2); + if (result.ratio < HIGH_SCORE_RATIO) + goto out; + g_ptr_array_add (sniffed_mime, g_strdup (result.type)); + + out: + g_ptr_array_add (sniffed_mime, NULL); + mimes = (gchar **) g_ptr_array_free (sniffed_mime, FALSE); + + g_array_free (results, TRUE); + g_task_return_pointer (self->priv->task, mimes, (GDestroyNotify)g_strfreev); +} + +/* adapted from nautilus/libnautilus-private/nautilus-directory-async.c */ +static void +deep_count_one (DeepCountState *state, + GFileInfo *info) +{ + GFile *subdir; + const char *content_type; + + if (g_file_info_get_file_type (info) == G_FILE_TYPE_DIRECTORY) + { + /* record the fact that we have to descend into this directory */ + subdir = g_file_get_child (state->file, g_file_info_get_name (info)); + state->deep_count_subdirectories = + g_list_append (state->deep_count_subdirectories, subdir); + } + else + { + content_type = g_file_info_get_content_type (info); + + if (content_type) + add_content_type_to_cache (state, content_type); + } +} + +static void +deep_count_finish (DeepCountState *state) +{ + prepare_async_result (state); + + if (state->enumerator) + { + if (!g_file_enumerator_is_closed (state->enumerator)) + g_file_enumerator_close_async (state->enumerator, + 0, NULL, NULL, NULL); + + g_object_unref (state->enumerator); + } + + g_cancellable_reset (state->self->priv->cancellable); + g_clear_object (&state->file); + + g_list_free_full (state->deep_count_subdirectories, g_object_unref); + + g_free (state); +} + +static void +deep_count_next_dir (DeepCountState *state) +{ + GFile *new_file; + + g_clear_object (&state->file); + + if (state->deep_count_subdirectories != NULL) + { + /* Work on a new directory. */ + new_file = state->deep_count_subdirectories->data; + state->deep_count_subdirectories = + g_list_remove (state->deep_count_subdirectories, new_file); + + deep_count_load (state, new_file); + g_object_unref (new_file); + } + else + { + deep_count_finish (state); + } +} + +static void +deep_count_more_files_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + DeepCountState *state; + GList *files, *l; + GFileInfo *info; + + state = user_data; + + if (g_cancellable_is_cancelled (state->self->priv->cancellable)) + { + deep_count_finish (state); + return; + } + + files = g_file_enumerator_next_files_finish (state->enumerator, + res, NULL); + + for (l = files; l != NULL; l = l->next) + { + info = l->data; + deep_count_one (state, info); + g_object_unref (info); + } + + if (files == NULL) + { + g_file_enumerator_close_async (state->enumerator, 0, NULL, NULL, NULL); + g_object_unref (state->enumerator); + state->enumerator = NULL; + + deep_count_next_dir (state); + } + else + { + g_file_enumerator_next_files_async (state->enumerator, + DIRECTORY_LOAD_ITEMS_PER_CALLBACK, + G_PRIORITY_LOW, + state->self->priv->cancellable, + deep_count_more_files_callback, + state); + } + + g_list_free (files); +} + +static void +deep_count_callback (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + DeepCountState *state; + GFileEnumerator *enumerator; + + state = user_data; + + if (g_cancellable_is_cancelled (state->self->priv->cancellable)) + { + deep_count_finish (state); + return; + } + + enumerator = g_file_enumerate_children_finish (G_FILE (source_object), + res, NULL); + + if (enumerator == NULL) + { + deep_count_next_dir (state); + } + else + { + state->enumerator = enumerator; + g_file_enumerator_next_files_async (state->enumerator, + DIRECTORY_LOAD_ITEMS_PER_CALLBACK, + G_PRIORITY_LOW, + state->self->priv->cancellable, + deep_count_more_files_callback, + state); + } +} + +static void +deep_count_load (DeepCountState *state, + GFile *file) +{ + state->file = g_object_ref (file); + + g_file_enumerate_children_async (state->file, + LOADER_ATTRS, + G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, /* flags */ + G_PRIORITY_LOW, /* prio */ + state->self->priv->cancellable, + deep_count_callback, + state); +} + +static void +deep_count_start (ShellMimeSniffer *self) +{ + DeepCountState *state; + + state = g_new0 (DeepCountState, 1); + state->self = self; + + deep_count_load (state, self->priv->file); +} + +static void +query_info_async_ready_cb (GObject *source, + GAsyncResult *res, + gpointer user_data) +{ + GFileInfo *info; + GError *error = NULL; + ShellMimeSniffer *self = user_data; + + info = g_file_query_info_finish (G_FILE (source), + res, &error); + + if (error != NULL) + { + g_task_return_error (self->priv->task, error); + + return; + } + + if (g_file_info_get_file_type (info) != G_FILE_TYPE_DIRECTORY) + { + g_task_return_new_error (self->priv->task, + G_IO_ERROR, + G_IO_ERROR_NOT_DIRECTORY, + "Not a directory"); + + return; + } + + deep_count_start (self); +} + +static gboolean +watchdog_timeout_reached_cb (gpointer user_data) +{ + ShellMimeSniffer *self = user_data; + + self->priv->watchdog_id = 0; + g_cancellable_cancel (self->priv->cancellable); + + return FALSE; +} + +static void +start_loading_file (ShellMimeSniffer *self) +{ + g_file_query_info_async (self->priv->file, + LOADER_ATTRS, + G_FILE_QUERY_INFO_NONE, + G_PRIORITY_DEFAULT, + self->priv->cancellable, + query_info_async_ready_cb, + self); +} + +static void +shell_mime_sniffer_set_file (ShellMimeSniffer *self, + GFile *file) +{ + g_clear_object (&self->priv->file); + self->priv->file = g_object_ref (file); +} + +static void +shell_mime_sniffer_dispose (GObject *object) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + g_clear_object (&self->priv->file); + g_clear_object (&self->priv->cancellable); + g_clear_object (&self->priv->task); + + g_clear_handle_id (&self->priv->watchdog_id, g_source_remove); + + G_OBJECT_CLASS (shell_mime_sniffer_parent_class)->dispose (object); +} + +static void +shell_mime_sniffer_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + switch (prop_id) { + case PROP_FILE: + g_value_set_object (value, self->priv->file); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_mime_sniffer_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellMimeSniffer *self = SHELL_MIME_SNIFFER (object); + + switch (prop_id) { + case PROP_FILE: + shell_mime_sniffer_set_file (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_mime_sniffer_class_init (ShellMimeSnifferClass *klass) +{ + GObjectClass *oclass; + + oclass = G_OBJECT_CLASS (klass); + oclass->dispose = shell_mime_sniffer_dispose; + oclass->get_property = shell_mime_sniffer_get_property; + oclass->set_property = shell_mime_sniffer_set_property; + + properties[PROP_FILE] = + g_param_spec_object ("file", + "File", + "The loaded file", + G_TYPE_FILE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (oclass, NUM_PROPERTIES, properties); +} + +static void +shell_mime_sniffer_init (ShellMimeSniffer *self) +{ + self->priv = shell_mime_sniffer_get_instance_private (self); + init_mimetypes (); +} + +ShellMimeSniffer * +shell_mime_sniffer_new (GFile *file) +{ + return g_object_new (SHELL_TYPE_MIME_SNIFFER, + "file", file, + NULL); +} + +void +shell_mime_sniffer_sniff_async (ShellMimeSniffer *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_assert (self->priv->watchdog_id == 0); + g_assert (self->priv->task == NULL); + + self->priv->cancellable = g_cancellable_new (); + self->priv->task = g_task_new (self, self->priv->cancellable, + callback, user_data); + + self->priv->watchdog_id = + g_timeout_add (WATCHDOG_TIMEOUT, + watchdog_timeout_reached_cb, self); + g_source_set_name_by_id (self->priv->watchdog_id, "[gnome-shell] watchdog_timeout_reached_cb"); + + start_loading_file (self); +} + +gchar ** +shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_pointer (self->priv->task, error); +} diff --git a/src/hotplug-sniffer/shell-mime-sniffer.h b/src/hotplug-sniffer/shell-mime-sniffer.h new file mode 100644 index 0000000..3936eef --- /dev/null +++ b/src/hotplug-sniffer/shell-mime-sniffer.h @@ -0,0 +1,46 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * Author: Cosimo Cecchi + * + */ + +#ifndef __SHELL_MIME_SNIFFER_H__ +#define __SHELL_MIME_SNIFFER_H__ + +#include +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_MIME_SNIFFER (shell_mime_sniffer_get_type ()) +G_DECLARE_FINAL_TYPE (ShellMimeSniffer, shell_mime_sniffer, + SHELL, MIME_SNIFFER, GObject) + +ShellMimeSniffer *shell_mime_sniffer_new (GFile *file); + +void shell_mime_sniffer_sniff_async (ShellMimeSniffer *self, + GAsyncReadyCallback callback, + gpointer user_data); + +gchar ** shell_mime_sniffer_sniff_finish (ShellMimeSniffer *self, + GAsyncResult *res, + GError **error); + +G_END_DECLS + +#endif /* __SHELL_MIME_SNIFFER_H__ */ diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..2311a74 --- /dev/null +++ b/src/main.c @@ -0,0 +1,599 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#if defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) +#include +#endif +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shell-global.h" +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "st.h" + +extern GType gnome_shell_plugin_get_type (void); + +#define SHELL_DBUS_SERVICE "org.gnome.Shell" + +#define WM_NAME "GNOME Shell" +#define GNOME_WM_KEYBINDINGS "Mutter,GNOME Shell" + +static gboolean is_gdm_mode = FALSE; +static char *session_mode = NULL; +static int caught_signal = 0; + +#define DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER 1 +#define DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER 4 + +enum { + SHELL_DEBUG_BACKTRACE_WARNINGS = 1, + SHELL_DEBUG_BACKTRACE_SEGFAULTS = 2, +}; +static int _shell_debug; +static gboolean _tracked_signals[NSIG] = { 0 }; + +static void +shell_dbus_acquire_name (GDBusProxy *bus, + guint32 request_name_flags, + guint32 *request_name_result, + const gchar *name, + gboolean fatal) +{ + GError *error = NULL; + GVariant *request_name_variant; + + if (!(request_name_variant = g_dbus_proxy_call_sync (bus, + "RequestName", + g_variant_new ("(su)", name, request_name_flags), + 0, /* call flags */ + -1, /* timeout */ + NULL, /* cancellable */ + &error))) + { + g_printerr ("failed to acquire %s: %s\n", name, error->message); + g_clear_error (&error); + if (!fatal) + return; + exit (1); + } + g_variant_get (request_name_variant, "(u)", request_name_result); + g_variant_unref (request_name_variant); +} + +static void +shell_dbus_init (gboolean replace) +{ + GDBusConnection *session; + GDBusProxy *bus; + GError *error = NULL; + guint32 request_name_flags; + guint32 request_name_result; + + session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error); + + if (error) { + g_printerr ("Failed to connect to session bus: %s", error->message); + exit (1); + } + + bus = g_dbus_proxy_new_sync (session, + G_DBUS_PROXY_FLAGS_NONE, + NULL, /* interface info */ + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + NULL, /* cancellable */ + &error); + + if (!bus) + { + g_printerr ("Failed to get a session bus proxy: %s", error->message); + exit (1); + } + + request_name_flags = G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT; + if (replace) + request_name_flags |= G_BUS_NAME_OWNER_FLAGS_REPLACE; + + shell_dbus_acquire_name (bus, + request_name_flags, + &request_name_result, + SHELL_DBUS_SERVICE, TRUE); + if (!(request_name_result == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER + || request_name_result == DBUS_REQUEST_NAME_REPLY_ALREADY_OWNER)) + { + g_printerr (SHELL_DBUS_SERVICE " already exists on bus and --replace not specified\n"); + exit (1); + } + + g_object_unref (bus); + g_object_unref (session); +} + +static void +shell_introspection_init (void) +{ + + g_irepository_prepend_search_path (MUTTER_TYPELIB_DIR); + g_irepository_prepend_search_path (GNOME_SHELL_PKGLIBDIR); + + /* We need to explicitly add the directories where the private libraries are + * installed to the GIR's library path, so that they can be found at runtime + * when linking using DT_RUNPATH (instead of DT_RPATH), which is the default + * for some linkers (e.g. gold) and in some distros (e.g. Debian). + */ + g_irepository_prepend_library_path (MUTTER_TYPELIB_DIR); + g_irepository_prepend_library_path (GNOME_SHELL_PKGLIBDIR); +} + +static void +shell_fonts_init (void) +{ + CoglPangoFontMap *fontmap; + + /* Disable text mipmapping; it causes problems on pre-GEM Intel + * drivers and we should just be rendering text at the right + * size rather than scaling it. If we do effects where we dynamically + * zoom labels, then we might want to reconsider. + */ + fontmap = COGL_PANGO_FONT_MAP (clutter_get_font_map ()); + cogl_pango_font_map_set_use_mipmapping (fontmap, FALSE); +} + +static void +shell_profiler_init (void) +{ + ShellGlobal *global; + GjsProfiler *profiler; + GjsContext *context; + const char *enabled; + const char *fd_str; + int fd = -1; + + /* Sysprof uses the "GJS_TRACE_FD=N" environment variable to connect GJS + * profiler data to the combined Sysprof capture. Since we are in control of + * the GjsContext, we need to proxy this FD across to the GJS profiler. + */ + + fd_str = g_getenv ("GJS_TRACE_FD"); + enabled = g_getenv ("GJS_ENABLE_PROFILER"); + if (fd_str == NULL || enabled == NULL) + return; + + global = shell_global_get (); + g_return_if_fail (global); + + context = _shell_global_get_gjs_context (global); + g_return_if_fail (context); + + profiler = gjs_context_get_profiler (context); + g_return_if_fail (profiler); + + if (fd_str) + { + fd = atoi (fd_str); + + if (fd > 2) + { + gjs_profiler_set_fd (profiler, fd); + gjs_profiler_start (profiler); + } + } +} + +static void +shell_profiler_shutdown (void) +{ + ShellGlobal *global; + GjsProfiler *profiler; + GjsContext *context; + + global = shell_global_get (); + context = _shell_global_get_gjs_context (global); + profiler = gjs_context_get_profiler (context); + + if (profiler) + gjs_profiler_stop (profiler); +} + +static void +malloc_statistics_callback (ShellPerfLog *perf_log, + gpointer data) +{ +#if defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) +#ifdef HAVE_MALLINFO2 + struct mallinfo2 info = mallinfo2 (); +#else + struct mallinfo info = mallinfo (); +#endif + + shell_perf_log_update_statistic_i (perf_log, + "malloc.arenaSize", + info.arena); + shell_perf_log_update_statistic_i (perf_log, + "malloc.mmapSize", + info.hblkhd); + shell_perf_log_update_statistic_i (perf_log, + "malloc.usedSize", + info.uordblks); +#endif /* defined (HAVE_MALLINFO) || defined (HAVE_MALLINFO2) */ +} + +static void +shell_perf_log_init (void) +{ + ShellPerfLog *perf_log = shell_perf_log_get_default (); + + /* For probably historical reasons, mallinfo() defines the returned values, + * even those in bytes as int, not size_t. We're determined not to use + * more than 2G of malloc'ed memory, so are OK with that. + */ + shell_perf_log_define_statistic (perf_log, + "malloc.arenaSize", + "Amount of memory allocated by malloc() with brk(), in bytes", + "i"); + shell_perf_log_define_statistic (perf_log, + "malloc.mmapSize", + "Amount of memory allocated by malloc() with mmap(), in bytes", + "i"); + shell_perf_log_define_statistic (perf_log, + "malloc.usedSize", + "Amount of malloc'ed memory currently in use", + "i"); + + shell_perf_log_add_statistics_callback (perf_log, + malloc_statistics_callback, + NULL, NULL); +} + +static void +shell_a11y_init (void) +{ + cally_accessibility_init (); + + if (clutter_get_accessibility_enabled () == FALSE) + { + g_warning ("Accessibility: clutter has no accessibility enabled" + " skipping the atk-bridge load"); + } + else + { + atk_bridge_adaptor_init (NULL, NULL); + } +} + +static void +shell_init_debug (const char *debug_env) +{ + static const GDebugKey keys[] = { + { "backtrace-warnings", SHELL_DEBUG_BACKTRACE_WARNINGS }, + { "backtrace-segfaults", SHELL_DEBUG_BACKTRACE_SEGFAULTS }, + }; + + _shell_debug = g_parse_debug_string (debug_env, keys, + G_N_ELEMENTS (keys)); +} + +static GLogWriterOutput +default_log_writer (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + GLogWriterOutput output; + int i; + + output = g_log_writer_default (log_level, fields, n_fields, user_data); + + if ((_shell_debug & SHELL_DEBUG_BACKTRACE_WARNINGS) && + ((log_level & G_LOG_LEVEL_CRITICAL) || + (log_level & G_LOG_LEVEL_WARNING))) + { + const char *log_domain = NULL; + + for (i = 0; i < n_fields; i++) + { + if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) + { + log_domain = fields[i].value; + break; + } + } + + /* Filter out Gjs logs, those already have the stack */ + if (g_strcmp0 (log_domain, "Gjs") != 0) + gjs_dumpstack (); + } + + return output; +} + +static GLogWriterOutput +shut_up (GLogLevelFlags log_level, + const GLogField *fields, + gsize n_fields, + gpointer user_data) +{ + return (GLogWriterOutput) {0}; +} + +static void +dump_gjs_stack_alarm_sigaction (int signo) +{ + g_log_set_writer_func (g_log_writer_default, NULL, NULL); + g_warning ("Failed to dump Javascript stack, got stuck"); + g_log_set_writer_func (default_log_writer, NULL, NULL); + + raise (caught_signal); +} + +static void +dump_gjs_stack_on_signal_handler (int signo) +{ + struct sigaction sa = { .sa_handler = dump_gjs_stack_alarm_sigaction }; + gsize i; + + /* Ignore all the signals starting this point, a part the one we'll raise + * (which is implicitly ignored here through SA_RESETHAND), this is needed + * not to get this handler being called by other signals that we were + * tracking and that might be emitted by code called starting from now. + */ + for (i = 0; i < G_N_ELEMENTS (_tracked_signals); ++i) + { + if (_tracked_signals[i] && i != signo) + signal (i, SIG_IGN); + } + + /* Waiting at least 5 seconds for the dumpstack, if it fails, we raise the error */ + caught_signal = signo; + sigemptyset (&sa.sa_mask); + sigaction (SIGALRM, &sa, NULL); + + alarm (5); + gjs_dumpstack (); + alarm (0); + + raise (signo); +} + +static void +dump_gjs_stack_on_signal (int signo) +{ + struct sigaction sa = { + .sa_flags = SA_RESETHAND | SA_NODEFER, + .sa_handler = dump_gjs_stack_on_signal_handler, + }; + + sigemptyset (&sa.sa_mask); + + sigaction (signo, &sa, NULL); + _tracked_signals[signo] = TRUE; +} + +static gboolean +list_modes (const char *option_name, + const char *value, + gpointer data, + GError **error) +{ + ShellGlobal *global; + GjsContext *context; + const char *script; + int status; + + /* Many of our imports require global to be set, so rather than + * tayloring our imports carefully here to avoid that dependency, + * we just set it. + * ShellGlobal has some GTK+ dependencies, so initialize GTK+; we + * don't really care if it fails though (e.g. when running from a tty), + * so we mute all warnings */ + g_log_set_writer_func (shut_up, NULL, NULL); + gtk_init_check (NULL, NULL); + + _shell_global_init (NULL); + global = shell_global_get (); + context = _shell_global_get_gjs_context (global); + + shell_introspection_init (); + + script = "imports.ui.environment.init();" + "imports.ui.sessionMode.listModes();"; + if (!gjs_context_eval (context, script, -1, "
", &status, NULL)) + g_message ("Retrieving list of available modes failed."); + + g_object_unref (context); + exit (status); +} + +static gboolean +print_version (const gchar *option_name, + const gchar *value, + gpointer data, + GError **error) +{ + g_print ("GNOME Shell %s\n", VERSION); + exit (0); +} + +GOptionEntry gnome_shell_options[] = { + { + "version", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, + print_version, + N_("Print version"), + NULL + }, + { + "gdm-mode", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, + &is_gdm_mode, + N_("Mode used by GDM for login screen"), + NULL + }, + { + "mode", 0, 0, G_OPTION_ARG_STRING, + &session_mode, + N_("Use a specific mode, e.g. “gdm” for login screen"), + "MODE" + }, + { + "list-modes", 0, G_OPTION_FLAG_NO_ARG, G_OPTION_ARG_CALLBACK, + list_modes, + N_("List possible modes"), + NULL + }, + { NULL } +}; + +static gboolean +on_sigterm (gpointer user_data) +{ + MetaContext *context = META_CONTEXT (user_data); + + meta_context_terminate (context); + + return G_SOURCE_REMOVE; +} + +static void +init_signal_handlers (MetaContext *context) +{ + struct sigaction act = { 0 }; + sigset_t empty_mask; + + sigemptyset (&empty_mask); + act.sa_handler = SIG_IGN; + act.sa_mask = empty_mask; + act.sa_flags = 0; + if (sigaction (SIGPIPE, &act, NULL) < 0) + g_warning ("Failed to register SIGPIPE handler: %s", g_strerror (errno)); +#ifdef SIGXFSZ + if (sigaction (SIGXFSZ, &act, NULL) < 0) + g_warning ("Failed to register SIGXFSZ handler: %s", g_strerror (errno)); +#endif + + g_unix_signal_add (SIGTERM, on_sigterm, context); +} + +static void +change_to_home_directory (void) +{ + const char *home_dir; + + home_dir = g_get_home_dir (); + if (!home_dir) + return; + + if (chdir (home_dir) < 0) + g_warning ("Could not change to home directory %s", home_dir); +} + +int +main (int argc, char **argv) +{ + g_autoptr (MetaContext) context = NULL; + GError *error = NULL; + int ecode = EXIT_SUCCESS; + + bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); + bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); + textdomain (GETTEXT_PACKAGE); + + context = meta_create_context (WM_NAME); + meta_context_add_option_entries (context, gnome_shell_options, + GETTEXT_PACKAGE); + meta_context_add_option_group (context, g_irepository_get_option_group ()); + + session_mode = (char *) g_getenv ("GNOME_SHELL_SESSION_MODE"); + + if (!meta_context_configure (context, &argc, &argv, &error)) + { + g_printerr ("Failed to configure: %s", error->message); + return EXIT_FAILURE; + } + + meta_context_set_plugin_gtype (context, gnome_shell_plugin_get_type ()); + meta_context_set_gnome_wm_keybindings (context, GNOME_WM_KEYBINDINGS); + + init_signal_handlers (context); + change_to_home_directory (); + + if (!meta_context_setup (context, &error)) + { + g_printerr ("Failed to setup: %s", error->message); + return EXIT_FAILURE; + } + + /* FIXME: Add gjs API to set this stuff and don't depend on the + * environment. These propagate to child processes. + */ + g_setenv ("GJS_DEBUG_OUTPUT", "stderr", TRUE); + g_setenv ("GJS_DEBUG_TOPICS", "JS ERROR;JS LOG", TRUE); + + shell_init_debug (g_getenv ("SHELL_DEBUG")); + + shell_dbus_init (meta_context_is_replacing (context)); + shell_a11y_init (); + shell_perf_log_init (); + shell_introspection_init (); + shell_fonts_init (); + + g_log_set_writer_func (default_log_writer, NULL, NULL); + + /* Initialize the global object */ + if (session_mode == NULL) + session_mode = is_gdm_mode ? (char *)"gdm" : (char *)"user"; + + _shell_global_init ("session-mode", session_mode, NULL); + + dump_gjs_stack_on_signal (SIGABRT); + dump_gjs_stack_on_signal (SIGFPE); + dump_gjs_stack_on_signal (SIGIOT); + dump_gjs_stack_on_signal (SIGTRAP); + + if ((_shell_debug & SHELL_DEBUG_BACKTRACE_SEGFAULTS)) + { + dump_gjs_stack_on_signal (SIGBUS); + dump_gjs_stack_on_signal (SIGSEGV); + } + + shell_profiler_init (); + + if (meta_context_get_compositor_type (context) == META_COMPOSITOR_TYPE_WAYLAND) + meta_context_raise_rlimit_nofile (context, NULL); + + if (!meta_context_start (context, &error)) + { + g_printerr ("GNOME Shell failed to start: %s", error->message); + return EXIT_FAILURE; + } + + if (!meta_context_run_main_loop (context, &error)) + { + g_printerr ("GNOME Shell terminated with an error: %s", error->message); + ecode = EXIT_FAILURE; + } + + meta_context_destroy (g_steal_pointer (&context)); + + shell_profiler_shutdown (); + +#if 0 + g_debug ("Doing final cleanup"); + _shell_global_destroy_gjs_context (shell_global_get ()); + g_object_unref (shell_global_get ()); +#endif + + return ecode; +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..fc7f8bf --- /dev/null +++ b/src/meson.build @@ -0,0 +1,276 @@ +service_data = configuration_data() +service_data.set('libexecdir', libexecdir) + +subdir('calendar-server') +subdir('hotplug-sniffer') +subdir('st') +subdir('tray') + +script_data = configuration_data() +script_data.set('bindir', bindir) +script_data.set('datadir', datadir) +script_data.set('libdir', libdir) +script_data.set('libexecdir', libexecdir) +script_data.set('pkgdatadir', pkgdatadir) +script_data.set('pkglibdir', pkglibdir) +script_data.set('PYTHON', python.full_path()) +script_data.set('VERSION', meson.project_version()) + +script_tools = ['gnome-shell-perf-tool'] + +if get_option('extensions_tool') + script_tools += 'gnome-shell-extension-tool' +endif + +foreach tool : script_tools + configure_file( + input: tool + '.in', + output: tool, + configuration: script_data, + install_dir: bindir + ) +endforeach + +install_data('gnome-shell-extension-prefs', + install_dir: bindir +) + +gnome_shell_cflags = [ + '-DCLUTTER_ENABLE_EXPERIMENTAL_API', + '-DCOGL_ENABLE_EXPERIMENTAL_API', + '-DVERSION="@0@"'.format(meson.project_version()), + '-DLOCALEDIR="@0@"'.format(localedir), + '-DDATADIR="@0@"'.format(datadir), + '-DGNOME_SHELL_LIBEXECDIR="@0@"'.format(libexecdir), + '-DGNOME_SHELL_DATADIR="@0@"'.format(pkgdatadir), + '-DGNOME_SHELL_PKGLIBDIR="@0@"'.format(pkglibdir) +] + +install_rpath = ':'.join([mutter_typelibdir, pkglibdir]) + +gnome_shell_deps = [ + gio_unix_dep, + libxml_dep, + gtk_dep, + atk_bridge_dep, + gjs_dep, + gdk_x11_dep, + clutter_dep, + cogl_pango_dep, + startup_dep, + gi_dep, + polkit_dep, + gcr_dep, + libsystemd_dep +] + +gnome_shell_deps += nm_deps + +tools_cflags = '-DLOCALEDIR="@0@"'.format(localedir) +tools_deps = [gio_dep, gjs_dep] + +libshell_menu_sources = [ + 'gtkactionmuxer.h', + 'gtkactionmuxer.c', + 'gtkactionobservable.h', + 'gtkactionobservable.c', + 'gtkactionobserver.h', + 'gtkactionobserver.c' +] + +libshell_menu = library('gnome-shell-menu', + sources: libshell_menu_sources, + dependencies: [gio_dep, clutter_dep], + include_directories: conf_inc, + build_rpath: mutter_typelibdir, + install_rpath: mutter_typelibdir, + install_dir: pkglibdir, + install: true +) + +libshell_menu_dep = declare_dependency(link_with: libshell_menu) + +libshell_public_headers = [ + 'shell-app.h', + 'shell-app-system.h', + 'shell-app-usage.h', + 'shell-blur-effect.h', + 'shell-embedded-window.h', + 'shell-glsl-effect.h', + 'shell-gtk-embed.h', + 'shell-global.h', + 'shell-invert-lightness-effect.h', + 'shell-action-modes.h', + 'shell-mount-operation.h', + 'shell-perf-log.h', + 'shell-screenshot.h', + 'shell-square-bin.h', + 'shell-stack.h', + 'shell-tray-icon.h', + 'shell-tray-manager.h', + 'shell-util.h', + 'shell-window-preview.h', + 'shell-window-preview-layout.h', + 'shell-window-tracker.h', + 'shell-wm.h', + 'shell-workspace-background.h' +] + +if have_networkmanager + libshell_public_headers += 'shell-network-agent.h' +endif + +libshell_private_headers = [ + 'shell-app-private.h', + 'shell-app-cache-private.h', + 'shell-app-system-private.h', + 'shell-global-private.h', + 'shell-window-tracker-private.h', + 'shell-wm-private.h' +] + +libshell_sources = [ + 'gnome-shell-plugin.c', + 'shell-app.c', + 'shell-app-system.c', + 'shell-app-usage.c', + 'shell-blur-effect.c', + 'shell-embedded-window.c', + 'shell-embedded-window-private.h', + 'shell-global.c', + 'shell-glsl-effect.c', + 'shell-gtk-embed.c', + 'shell-invert-lightness-effect.c', + 'shell-keyring-prompt.c', + 'shell-keyring-prompt.h', + 'shell-mount-operation.c', + 'shell-perf-log.c', + 'shell-polkit-authentication-agent.c', + 'shell-polkit-authentication-agent.h', + 'shell-screenshot.c', + 'shell-secure-text-buffer.c', + 'shell-secure-text-buffer.h', + 'shell-square-bin.c', + 'shell-stack.c', + 'shell-tray-icon.c', + 'shell-tray-manager.c', + 'shell-util.c', + 'shell-window-preview.c', + 'shell-window-preview-layout.c', + 'shell-window-tracker.c', + 'shell-wm.c', + 'shell-workspace-background.c' +] + +if have_networkmanager + libshell_sources += 'shell-network-agent.c' +endif + +libshell_private_sources = [ + 'shell-app-cache.c', +] + +libshell_enums = gnome.mkenums_simple('shell-enum-types', + sources: libshell_public_headers +) + +libshell_gir_sources = [ + libshell_enums, + libshell_public_headers, + libshell_sources +] + +libshell_no_gir_sources = [ + js_resources, + libshell_private_headers, + libshell_private_sources +] + +dbus_generated = gnome.gdbus_codegen('org-gtk-application', + 'org.gtk.Application.xml', + namespace: 'Shell' +) + +dbus_generated += gnome.gdbus_codegen('switcheroo-control', + '../data/dbus-interfaces/net.hadess.SwitcherooControl.xml', + namespace: 'Shell' +) + +libshell_no_gir_sources += dbus_generated + +libshell = library('gnome-shell', + sources: libshell_gir_sources + libshell_no_gir_sources, + dependencies: gnome_shell_deps + [libshell_menu_dep, libst_dep, mutter_dep, gnome_desktop_dep, m_dep], + include_directories: [conf_inc, st_inc, include_directories('tray')], + c_args: gnome_shell_cflags, + link_with: [libtray], + build_rpath: mutter_typelibdir, + install_rpath: install_rpath, + install_dir: pkglibdir, + install: true +) + +libshell_dep = declare_dependency(link_with: libshell) + +libshell_gir_includes = [ + 'Clutter-@0@'.format(mutter_api_version), + 'Meta-@0@'.format(mutter_api_version), + 'Gcr-4', + 'PolkitAgent-1.0' +] + +if have_networkmanager + libshell_gir_includes += ['NM-1.0'] +endif + +libshell_gir_includes += [ + libgvc_gir[0], + libst_gir[0] +] + +gnome.generate_gir(libshell, + sources: libshell_gir_sources, + nsversion: '0.1', + namespace: 'Shell', + includes: libshell_gir_includes, + extra_args: ['--quiet'], + install_dir_gir: pkgdatadir, + install_dir_typelib: pkglibdir, + install: true +) + +executable('gnome-shell', 'main.c', + c_args: gnome_shell_cflags + [ + '-DMUTTER_TYPELIB_DIR="@0@"'.format(mutter_typelibdir) + ], + dependencies: gnome_shell_deps + [libshell_dep, libst_dep, mutter_dep], + include_directories: [conf_inc, st_inc, include_directories('tray')], + build_rpath: mutter_typelibdir, + install_rpath: install_rpath, + install: true +) + +if have_networkmanager + executable('gnome-shell-portal-helper', + 'gnome-shell-portal-helper.c', portal_resources, + c_args: tools_cflags, + dependencies: tools_deps, + include_directories: [conf_inc], + install_dir: libexecdir, + install: true + ) +endif + +executable('gnome-shell-perf-helper', 'shell-perf-helper.c', + dependencies: [gtk_dep, gio_dep, m_dep], + include_directories: [conf_inc], + install_dir: libexecdir, + install: true +) + +executable('run-js-test', 'run-js-test.c', + dependencies: [mutter_dep, gio_dep, gi_dep, gjs_dep], + include_directories: [conf_inc], + link_with: libshell, + build_rpath: mutter_typelibdir, +) diff --git a/src/org.gtk.Application.xml b/src/org.gtk.Application.xml new file mode 100644 index 0000000..161aa1d --- /dev/null +++ b/src/org.gtk.Application.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/run-js-test.c b/src/run-js-test.c new file mode 100644 index 0000000..ba5e875 --- /dev/null +++ b/src/run-js-test.c @@ -0,0 +1,118 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Based on gjs/console.c from GJS + * + * Copyright (c) 2008 litl, LLC + * Copyright (c) 2010 Red Hat, Inc. + * + * 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. + */ + +#include "config.h" + +#include +#include +#include + +#include +#include + +#include "shell-global.h" +#include "shell-global-private.h" + +static char *command = NULL; + +static GOptionEntry entries[] = { + { "command", 'c', 0, G_OPTION_ARG_STRING, &command, "Program passed in as a string", "COMMAND" }, + { NULL } +}; + +int +main(int argc, char **argv) +{ + GOptionContext *context; + GError *error = NULL; + ShellGlobal *global; + GjsContext *js_context; + char *script; + const char *filename; + char *title; + gsize len; + int code; + + context = g_option_context_new (NULL); + + /* pass unknown through to the JS script */ + g_option_context_set_ignore_unknown_options (context, TRUE); + + g_option_context_add_main_entries (context, entries, NULL); + if (!g_option_context_parse (context, &argc, &argv, &error)) + g_error ("option parsing failed: %s", error->message); + + setlocale (LC_ALL, ""); + + _shell_global_init (NULL); + global = shell_global_get (); + js_context = _shell_global_get_gjs_context (global); + + /* prepare command line arguments */ + if (!gjs_context_define_string_array (js_context, "ARGV", + argc - 2, (const char**)argv + 2, + &error)) { + g_printerr ("Failed to defined ARGV: %s", error->message); + exit (1); + } + + if (command != NULL) { + script = command; + len = strlen (script); + filename = ""; + } else if (argc <= 1) { + script = g_strdup ("const Console = imports.console; Console.interact();"); + len = strlen (script); + filename = ""; + } else /*if (argc >= 2)*/ { + error = NULL; + if (!g_file_get_contents (argv[1], &script, &len, &error)) { + g_printerr ("%s\n", error->message); + exit (1); + } + filename = argv[1]; + } + + title = g_filename_display_basename (filename); + g_set_prgname (title); + g_free (title); + + /* evaluate the script */ + error = NULL; + if (!gjs_context_eval (js_context, script, len, + filename, &code, &error)) { + g_free (script); + g_printerr ("%s\n", error->message); + exit (1); + } + + gjs_context_gc (js_context); + gjs_context_gc (js_context); + + g_object_unref (js_context); + g_free (script); + exit (code); +} diff --git a/src/shell-action-modes.h b/src/shell-action-modes.h new file mode 100644 index 0000000..edbdd16 --- /dev/null +++ b/src/shell-action-modes.h @@ -0,0 +1,35 @@ +/** + * ShellActionMode: + * @SHELL_ACTION_MODE_NONE: block action + * @SHELL_ACTION_MODE_NORMAL: allow action when in window mode, + * e.g. when the focus is in an application window + * @SHELL_ACTION_MODE_OVERVIEW: allow action while the overview + * is active + * @SHELL_ACTION_MODE_LOCK_SCREEN: allow action when the screen + * is locked, e.g. when the screen shield is shown + * @SHELL_ACTION_MODE_UNLOCK_SCREEN: allow action in the unlock + * dialog + * @SHELL_ACTION_MODE_LOGIN_SCREEN: allow action in the login screen + * @SHELL_ACTION_MODE_SYSTEM_MODAL: allow action when a system modal + * dialog (e.g. authentication or session dialogs) is open + * @SHELL_ACTION_MODE_LOOKING_GLASS: allow action in looking glass + * @SHELL_ACTION_MODE_POPUP: allow action while a shell menu is open + * @SHELL_ACTION_MODE_ALL: always allow action + * + * Controls in which GNOME Shell states an action (like keybindings and gestures) + * should be handled. +*/ +typedef enum { + SHELL_ACTION_MODE_NONE = 0, + SHELL_ACTION_MODE_NORMAL = 1 << 0, + SHELL_ACTION_MODE_OVERVIEW = 1 << 1, + SHELL_ACTION_MODE_LOCK_SCREEN = 1 << 2, + SHELL_ACTION_MODE_UNLOCK_SCREEN = 1 << 3, + SHELL_ACTION_MODE_LOGIN_SCREEN = 1 << 4, + SHELL_ACTION_MODE_SYSTEM_MODAL = 1 << 5, + SHELL_ACTION_MODE_LOOKING_GLASS = 1 << 6, + SHELL_ACTION_MODE_POPUP = 1 << 7, + + SHELL_ACTION_MODE_ALL = ~0, +} ShellActionMode; + diff --git a/src/shell-app-cache-private.h b/src/shell-app-cache-private.h new file mode 100644 index 0000000..b73094a --- /dev/null +++ b/src/shell-app-cache-private.h @@ -0,0 +1,19 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_CACHE_PRIVATE_H__ +#define __SHELL_APP_CACHE_PRIVATE_H__ + +#include +#include + +#define SHELL_TYPE_APP_CACHE (shell_app_cache_get_type()) + +G_DECLARE_FINAL_TYPE (ShellAppCache, shell_app_cache, SHELL, APP_CACHE, GObject) + +ShellAppCache *shell_app_cache_get_default (void); +GList *shell_app_cache_get_all (ShellAppCache *cache); +GDesktopAppInfo *shell_app_cache_get_info (ShellAppCache *cache, + const char *id); +char *shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name); + +#endif /* __SHELL_APP_CACHE_PRIVATE_H__ */ diff --git a/src/shell-app-cache.c b/src/shell-app-cache.c new file mode 100644 index 0000000..44fc8b0 --- /dev/null +++ b/src/shell-app-cache.c @@ -0,0 +1,404 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-app-cache-private.h" + +/** + * SECTION:shell-app-cache + * @title: ShellAppCache + * @short_description: application information cache + * + * The #ShellAppCache is responsible for caching information about #GAppInfo + * to ensure that the compositor thread never needs to perform disk reads to + * access them. All of the work is done off-thread. When the new data has + * been loaded, a #ShellAppCache::changed signal is emitted. + * + * Additionally, the #ShellAppCache caches information about translations for + * directories. This allows translation provided in [Desktop Entry] GKeyFiles + * to be available when building StLabel and other elements without performing + * costly disk reads. + * + * Various monitors are used to keep this information up to date while the + * Shell is running. + */ + +#define DEFAULT_TIMEOUT_SECONDS 5 + +struct _ShellAppCache +{ + GObject parent_instance; + + GAppInfoMonitor *monitor; + GPtrArray *dir_monitors; + GHashTable *folders; + GCancellable *cancellable; + GList *app_infos; + + guint queued_update; +}; + +typedef struct +{ + GList *app_infos; + GHashTable *folders; +} CacheState; + +G_DEFINE_TYPE (ShellAppCache, shell_app_cache, G_TYPE_OBJECT) + +enum { + CHANGED, + N_SIGNALS +}; + +static guint signals [N_SIGNALS]; + +static void +cache_state_free (CacheState *state) +{ + g_clear_pointer (&state->folders, g_hash_table_unref); + g_list_free_full (state->app_infos, g_object_unref); + g_free (state); +} + +static CacheState * +cache_state_new (void) +{ + CacheState *state; + + state = g_new0 (CacheState, 1); + state->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + return g_steal_pointer (&state); +} + +/** + * shell_app_cache_get_default: + * + * Gets the default #ShellAppCache. + * + * Returns: (transfer none): a #ShellAppCache + */ +ShellAppCache * +shell_app_cache_get_default (void) +{ + static ShellAppCache *instance; + + if (instance == NULL) + { + instance = g_object_new (SHELL_TYPE_APP_CACHE, NULL); + g_object_add_weak_pointer (G_OBJECT (instance), (gpointer *)&instance); + } + + return instance; +} + +static void +load_folder (GHashTable *folders, + const char *path) +{ + g_autoptr(GDir) dir = NULL; + const char *name; + + g_assert (folders != NULL); + g_assert (path != NULL); + + dir = g_dir_open (path, 0, NULL); + if (dir == NULL) + return; + + while ((name = g_dir_read_name (dir))) + { + g_autofree gchar *filename = NULL; + g_autoptr(GKeyFile) keyfile = NULL; + + /* First added wins */ + if (g_hash_table_contains (folders, name)) + continue; + + filename = g_build_filename (path, name, NULL); + keyfile = g_key_file_new (); + + if (g_key_file_load_from_file (keyfile, filename, G_KEY_FILE_NONE, NULL)) + { + gchar *translated; + + translated = g_key_file_get_locale_string (keyfile, + "Desktop Entry", "Name", + NULL, NULL); + + if (translated != NULL) + g_hash_table_insert (folders, g_strdup (name), translated); + } + } +} + +static void +load_folders (GHashTable *folders) +{ + const char * const *dirs; + g_autofree gchar *userdir = NULL; + guint i; + + g_assert (folders != NULL); + + userdir = g_build_filename (g_get_user_data_dir (), "desktop-directories", NULL); + load_folder (folders, userdir); + + dirs = g_get_system_data_dirs (); + for (i = 0; dirs[i] != NULL; i++) + { + g_autofree gchar *sysdir = g_build_filename (dirs[i], "desktop-directories", NULL); + load_folder (folders, sysdir); + } +} + +static void +shell_app_cache_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + CacheState *state; + + g_assert (G_IS_TASK (task)); + g_assert (SHELL_IS_APP_CACHE (source_object)); + + state = cache_state_new (); + state->app_infos = g_app_info_get_all (); + load_folders (state->folders); + + g_task_return_pointer (task, state, (GDestroyNotify) cache_state_free); +} + +static void +apply_update_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellAppCache *cache = (ShellAppCache *)object; + g_autoptr(GError) error = NULL; + CacheState *state; + + g_assert (SHELL_IS_APP_CACHE (cache)); + g_assert (G_IS_TASK (result)); + g_assert (user_data == NULL); + + state = g_task_propagate_pointer (G_TASK (result), &error); + + if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + return; + + g_list_free_full (cache->app_infos, g_object_unref); + cache->app_infos = g_steal_pointer (&state->app_infos); + + g_clear_pointer (&cache->folders, g_hash_table_unref); + cache->folders = g_steal_pointer (&state->folders); + + g_signal_emit (cache, signals[CHANGED], 0); + + cache_state_free (state); +} + +static gboolean +shell_app_cache_do_update (gpointer user_data) +{ + ShellAppCache *cache = user_data; + g_autoptr(GTask) task = NULL; + + cache->queued_update = 0; + + /* Reset the cancellable state so we don't race with + * two updates coming back overlapped and applying the + * information in the wrong order. + */ + g_cancellable_cancel (cache->cancellable); + g_clear_object (&cache->cancellable); + cache->cancellable = g_cancellable_new (); + + task = g_task_new (cache, cache->cancellable, apply_update_cb, NULL); + g_task_set_source_tag (task, shell_app_cache_do_update); + g_task_run_in_thread (task, shell_app_cache_worker); + + return G_SOURCE_REMOVE; +} + +static void +shell_app_cache_queue_update (ShellAppCache *self) +{ + g_assert (SHELL_IS_APP_CACHE (self)); + + if (self->queued_update != 0) + g_source_remove (self->queued_update); + + self->queued_update = g_timeout_add_seconds (DEFAULT_TIMEOUT_SECONDS, + shell_app_cache_do_update, + self); +} + +static void +monitor_desktop_directories_for_data_dir (ShellAppCache *self, + const gchar *directory) +{ + g_autofree gchar *subdir = NULL; + g_autoptr(GFile) file = NULL; + g_autoptr(GFileMonitor) monitor = NULL; + + g_assert (SHELL_IS_APP_CACHE (self)); + + if (directory == NULL) + return; + + subdir = g_build_filename (directory, "desktop-directories", NULL); + file = g_file_new_for_path (subdir); + monitor = g_file_monitor_directory (file, G_FILE_MONITOR_NONE, NULL, NULL); + + if (monitor != NULL) + { + g_file_monitor_set_rate_limit (monitor, DEFAULT_TIMEOUT_SECONDS * 1000); + g_signal_connect_object (monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + g_ptr_array_add (self->dir_monitors, g_steal_pointer (&monitor)); + } +} + +static void +shell_app_cache_finalize (GObject *object) +{ + ShellAppCache *self = (ShellAppCache *)object; + + g_clear_object (&self->monitor); + + if (self->queued_update) + { + g_source_remove (self->queued_update); + self->queued_update = 0; + } + + g_clear_pointer (&self->dir_monitors, g_ptr_array_unref); + g_clear_pointer (&self->folders, g_hash_table_unref); + g_list_free_full (self->app_infos, g_object_unref); + + G_OBJECT_CLASS (shell_app_cache_parent_class)->finalize (object); +} + +static void +shell_app_cache_class_init (ShellAppCacheClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = shell_app_cache_finalize; + + /** + * ShellAppCache::changed: + * + * The "changed" signal is emitted when the cache has updated + * information about installed applications. + */ + 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 +shell_app_cache_init (ShellAppCache *self) +{ + const gchar * const *sysdirs; + guint i; + + /* Monitor directories for translation changes */ + self->dir_monitors = g_ptr_array_new_with_free_func (g_object_unref); + monitor_desktop_directories_for_data_dir (self, g_get_user_data_dir ()); + sysdirs = g_get_system_data_dirs (); + for (i = 0; sysdirs[i] != NULL; i++) + monitor_desktop_directories_for_data_dir (self, sysdirs[i]); + + /* Load translated directory names immediately */ + self->folders = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + load_folders (self->folders); + + /* Setup AppMonitor to track changes */ + self->monitor = g_app_info_monitor_get (); + g_signal_connect_object (self->monitor, + "changed", + G_CALLBACK (shell_app_cache_queue_update), + self, + G_CONNECT_SWAPPED); + self->app_infos = g_app_info_get_all (); +} + +/** + * shell_app_cache_get_all: + * @cache: (nullable): a #ShellAppCache or %NULL + * + * Like g_app_info_get_all() but always returns a + * cached set of application info so the caller can be + * sure that I/O will not happen on the current thread. + * + * Returns: (transfer none) (element-type GAppInfo): + * a #GList of references to #GAppInfo. + */ +GList * +shell_app_cache_get_all (ShellAppCache *cache) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + return cache->app_infos; +} + +/** + * shell_app_cache_get_info: + * @cache: (nullable): a #ShellAppCache or %NULL + * @id: the application id + * + * A replacement for g_desktop_app_info_new() that will lookup the + * information from the cache instead of (re)loading from disk. + * + * Returns: (nullable) (transfer none): a #GDesktopAppInfo or %NULL + */ +GDesktopAppInfo * +shell_app_cache_get_info (ShellAppCache *cache, + const char *id) +{ + const GList *iter; + + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + for (iter = cache->app_infos; iter != NULL; iter = iter->next) + { + GAppInfo *info = iter->data; + + if (g_strcmp0 (id, g_app_info_get_id (info)) == 0) + return G_DESKTOP_APP_INFO (info); + } + + return NULL; +} + +/** + * shell_app_cache_translate_folder: + * @cache: (nullable): a #ShellAppCache or %NULL + * @name: the folder name + * + * Gets the translated folder name for @name if any exists. + * + * Returns: (nullable): the translated string or %NULL if there is no + * translation. + */ +char * +shell_app_cache_translate_folder (ShellAppCache *cache, + const char *name) +{ + g_return_val_if_fail (SHELL_IS_APP_CACHE (cache), NULL); + + if (name == NULL) + return NULL; + + return g_strdup (g_hash_table_lookup (cache->folders, name)); +} diff --git a/src/shell-app-private.h b/src/shell-app-private.h new file mode 100644 index 0000000..b1786b3 --- /dev/null +++ b/src/shell-app-private.h @@ -0,0 +1,24 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_PRIVATE_H__ +#define __SHELL_APP_PRIVATE_H__ + +#include "shell-app.h" +#include "shell-app-system.h" + +G_BEGIN_DECLS + +ShellApp* _shell_app_new_for_window (MetaWindow *window); + +ShellApp* _shell_app_new (GDesktopAppInfo *info); + +void _shell_app_set_app_info (ShellApp *app, GDesktopAppInfo *info); + +void _shell_app_handle_startup_sequence (ShellApp *app, MetaStartupSequence *sequence); + +void _shell_app_add_window (ShellApp *app, MetaWindow *window); + +void _shell_app_remove_window (ShellApp *app, MetaWindow *window); + +G_END_DECLS + +#endif /* __SHELL_APP_PRIVATE_H__ */ diff --git a/src/shell-app-system-private.h b/src/shell-app-system-private.h new file mode 100644 index 0000000..975d563 --- /dev/null +++ b/src/shell-app-system-private.h @@ -0,0 +1,9 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_SYSTEM_PRIVATE_H__ +#define __SHELL_APP_SYSTEM_PRIVATE_H__ + +#include "shell-app-system.h" + +void _shell_app_system_notify_app_state_changed (ShellAppSystem *self, ShellApp *app); + +#endif diff --git a/src/shell-app-system.c b/src/shell-app-system.c new file mode 100644 index 0000000..2899544 --- /dev/null +++ b/src/shell-app-system.c @@ -0,0 +1,586 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-app-system.h" +#include "shell-app-usage.h" +#include + +#include +#include + +#include "shell-app-cache-private.h" +#include "shell-app-private.h" +#include "shell-window-tracker-private.h" +#include "shell-app-system-private.h" +#include "shell-global.h" +#include "shell-util.h" +#include "st.h" + +/* Rescan for at most RESCAN_TIMEOUT_MS * MAX_RESCAN_RETRIES. That + * should be plenty of time for even a slow spinning drive to update + * the icon cache. + */ +#define RESCAN_TIMEOUT_MS 2500 +#define MAX_RESCAN_RETRIES 6 + +/* Vendor prefixes are something that can be preprended to a .desktop + * file name. Undo this. + */ +static const char*const vendor_prefixes[] = { "gnome-", + "fedora-", + "mozilla-", + "debian-", + NULL }; + +enum { + PROP_0, + +}; + +enum { + APP_STATE_CHANGED, + INSTALLED_CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +typedef struct _ShellAppSystemPrivate ShellAppSystemPrivate; + +struct _ShellAppSystem +{ + GObject parent; + + ShellAppSystemPrivate *priv; +}; + +struct _ShellAppSystemPrivate { + GHashTable *running_apps; + GHashTable *id_to_app; + GHashTable *startup_wm_class_to_id; + GList *installed_apps; + + guint rescan_icons_timeout_id; + guint n_rescan_retries; +}; + +static void shell_app_system_finalize (GObject *object); + +G_DEFINE_TYPE_WITH_PRIVATE (ShellAppSystem, shell_app_system, G_TYPE_OBJECT); + +static void shell_app_system_class_init(ShellAppSystemClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *)klass; + + gobject_class->finalize = shell_app_system_finalize; + + signals[APP_STATE_CHANGED] = g_signal_new ("app-state-changed", + SHELL_TYPE_APP_SYSTEM, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 1, + SHELL_TYPE_APP); + signals[INSTALLED_CHANGED] = + g_signal_new ("installed-changed", + SHELL_TYPE_APP_SYSTEM, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +/* + * Check whether @wm_class matches @id exactly when ignoring the .desktop suffix + */ +static gboolean +startup_wm_class_is_exact_match (const char *id, + const char *wm_class) +{ + size_t wm_class_len; + + if (!g_str_has_prefix (id, wm_class)) + return FALSE; + + wm_class_len = strlen (wm_class); + if (id[wm_class_len] == '\0') + return TRUE; + + return g_str_equal (id + wm_class_len, ".desktop"); +} + +static void +scan_startup_wm_class_to_id (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv = self->priv; + g_autoptr(GPtrArray) no_show_ids = NULL; + const GList *l; + GList *all; + + g_hash_table_remove_all (priv->startup_wm_class_to_id); + + all = shell_app_cache_get_all (shell_app_cache_get_default ()); + no_show_ids = g_ptr_array_new (); + + for (l = all; l != NULL; l = l->next) + { + GAppInfo *info = l->data; + const char *startup_wm_class, *id, *old_id; + gboolean should_show; + + id = g_app_info_get_id (info); + startup_wm_class = g_desktop_app_info_get_startup_wm_class (G_DESKTOP_APP_INFO (info)); + + if (startup_wm_class == NULL) + continue; + + should_show = g_app_info_should_show (info); + if (!should_show) + g_ptr_array_add (no_show_ids, (char *) id); + + /* In case multiple .desktop files set the same StartupWMClass, prefer + * the one where ID and StartupWMClass match */ + old_id = g_hash_table_lookup (priv->startup_wm_class_to_id, startup_wm_class); + + if (old_id && startup_wm_class_is_exact_match (id, startup_wm_class)) + old_id = NULL; + + /* Give priority to the desktop files that should be shown */ + if (old_id && should_show && + g_ptr_array_find_with_equal_func (no_show_ids, old_id, g_str_equal, NULL)) + old_id = NULL; + + if (!old_id) + g_hash_table_insert (priv->startup_wm_class_to_id, + g_strdup (startup_wm_class), g_strdup (id)); + } +} + +static gboolean +app_is_stale (ShellApp *app) +{ + GDesktopAppInfo *info, *old; + GAppInfo *old_info, *new_info; + gboolean is_unchanged; + + if (shell_app_is_window_backed (app)) + return FALSE; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), + shell_app_get_id (app)); + if (!info) + return TRUE; + + old = shell_app_get_app_info (app); + old_info = G_APP_INFO (old); + new_info = G_APP_INFO (info); + + is_unchanged = + g_app_info_should_show (old_info) == g_app_info_should_show (new_info) && + strcmp (g_desktop_app_info_get_filename (old), + g_desktop_app_info_get_filename (info)) == 0 && + g_strcmp0 (g_app_info_get_executable (old_info), + g_app_info_get_executable (new_info)) == 0 && + g_strcmp0 (g_app_info_get_commandline (old_info), + g_app_info_get_commandline (new_info)) == 0 && + strcmp (g_app_info_get_name (old_info), + g_app_info_get_name (new_info)) == 0 && + g_strcmp0 (g_app_info_get_description (old_info), + g_app_info_get_description (new_info)) == 0 && + strcmp (g_app_info_get_display_name (old_info), + g_app_info_get_display_name (new_info)) == 0 && + g_icon_equal (g_app_info_get_icon (old_info), + g_app_info_get_icon (new_info)); + + return !is_unchanged; +} + +static gboolean +stale_app_remove_func (gpointer key, + gpointer value, + gpointer user_data) +{ + return app_is_stale (value); +} + +static void +collect_stale_windows (gpointer key, + gpointer value, + gpointer user_data) +{ + ShellApp *app = key; + GDesktopAppInfo *info; + GPtrArray *windows = user_data; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), + shell_app_get_id (app)); + + /* No info either means that the app became stale, or that it is + * window-backed. Re-tracking the app's windows allows us to reflect + * changes in either direction, i.e. from stale app to window-backed, + * or from window-backed to app-backed (if the app was launched right + * between installing the app and updating the app cache). + */ + if (info == NULL) + { + GSList *l; + + for (l = shell_app_get_windows (app); l; l = l->next) + g_ptr_array_add (windows, l->data); + } +} + +static void +retrack_window (gpointer data, + gpointer user_data) +{ + GObject *window = data; + + /* Make ShellWindowTracker retrack the window */ + g_object_notify (window, "wm-class"); +} + +static gboolean +rescan_icon_theme_cb (gpointer user_data) +{ + ShellAppSystemPrivate *priv; + ShellAppSystem *self; + StTextureCache *texture_cache; + gboolean rescanned; + + self = (ShellAppSystem *) user_data; + priv = self->priv; + + texture_cache = st_texture_cache_get_default (); + rescanned = st_texture_cache_rescan_icon_theme (texture_cache); + + priv->n_rescan_retries++; + + if (rescanned || priv->n_rescan_retries >= MAX_RESCAN_RETRIES) + { + priv->n_rescan_retries = 0; + priv->rescan_icons_timeout_id = 0; + return G_SOURCE_REMOVE; + } + + return G_SOURCE_CONTINUE; +} + +static void +rescan_icon_theme (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv = self->priv; + + priv->n_rescan_retries = 0; + + if (priv->rescan_icons_timeout_id > 0) + return; + + priv->rescan_icons_timeout_id = g_timeout_add (RESCAN_TIMEOUT_MS, + rescan_icon_theme_cb, + self); +} + +static void +installed_changed (ShellAppCache *cache, + ShellAppSystem *self) +{ + GPtrArray *windows = g_ptr_array_new (); + + rescan_icon_theme (self); + scan_startup_wm_class_to_id (self); + + g_hash_table_foreach_remove (self->priv->id_to_app, stale_app_remove_func, NULL); + g_hash_table_foreach (self->priv->running_apps, collect_stale_windows, windows); + + g_ptr_array_foreach (windows, retrack_window, NULL); + g_ptr_array_free (windows, TRUE); + + g_signal_emit (self, signals[INSTALLED_CHANGED], 0, NULL); +} + +static void +shell_app_system_init (ShellAppSystem *self) +{ + ShellAppSystemPrivate *priv; + ShellAppCache *cache; + + self->priv = priv = shell_app_system_get_instance_private (self); + + priv->running_apps = g_hash_table_new_full (NULL, NULL, (GDestroyNotify) g_object_unref, NULL); + priv->id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, + (GDestroyNotify)g_object_unref); + + priv->startup_wm_class_to_id = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + cache = shell_app_cache_get_default (); + g_signal_connect (cache, "changed", G_CALLBACK (installed_changed), self); + installed_changed (cache, self); +} + +static void +shell_app_system_finalize (GObject *object) +{ + ShellAppSystem *self = SHELL_APP_SYSTEM (object); + ShellAppSystemPrivate *priv = self->priv; + + g_hash_table_destroy (priv->running_apps); + g_hash_table_destroy (priv->id_to_app); + g_hash_table_destroy (priv->startup_wm_class_to_id); + g_list_free_full (priv->installed_apps, g_object_unref); + g_clear_handle_id (&priv->rescan_icons_timeout_id, g_source_remove); + + G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object); +} + +/** + * shell_app_system_get_default: + * + * Return Value: (transfer none): The global #ShellAppSystem singleton + */ +ShellAppSystem * +shell_app_system_get_default (void) +{ + static ShellAppSystem *instance = NULL; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL); + + return instance; +} + +/** + * shell_app_system_lookup_app: + * + * Find a #ShellApp corresponding to an id. + * + * Return value: (transfer none): The #ShellApp for id, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_app (ShellAppSystem *self, + const char *id) +{ + ShellAppSystemPrivate *priv = self->priv; + ShellApp *app; + GDesktopAppInfo *info; + + app = g_hash_table_lookup (priv->id_to_app, id); + if (app) + return app; + + info = shell_app_cache_get_info (shell_app_cache_get_default (), id); + if (!info) + return NULL; + + app = _shell_app_new (info); + g_hash_table_insert (priv->id_to_app, (char *) shell_app_get_id (app), app); + return app; +} + +/** + * shell_app_system_lookup_heuristic_basename: + * @system: a #ShellAppSystem + * @id: Probable application identifier + * + * Find a valid application corresponding to a given + * heuristically determined application identifier + * string, or %NULL if none. + * + * Returns: (transfer none): A #ShellApp for @name + */ +ShellApp * +shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *name) +{ + ShellApp *result; + const char *const *prefix; + + result = shell_app_system_lookup_app (system, name); + if (result != NULL) + return result; + + for (prefix = vendor_prefixes; *prefix != NULL; prefix++) + { + char *tmpid = g_strconcat (*prefix, name, NULL); + result = shell_app_system_lookup_app (system, tmpid); + g_free (tmpid); + if (result != NULL) + return result; + } + + return NULL; +} + +/** + * shell_app_system_lookup_desktop_wmclass: + * @system: a #ShellAppSystem + * @wmclass: (nullable): A WM_CLASS value + * + * Find a valid application whose .desktop file, without the extension + * and properly canonicalized, matches @wmclass. + * + * Returns: (transfer none): A #ShellApp for @wmclass + */ +ShellApp * +shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system, + const char *wmclass) +{ + char *canonicalized; + char *desktop_file; + ShellApp *app; + + if (wmclass == NULL) + return NULL; + + /* First try without changing the case (this handles + org.example.Foo.Bar.desktop applications) + + Note that is slightly wrong in that Gtk+ would set + the WM_CLASS to Org.example.Foo.Bar, but it also + sets the instance part to org.example.Foo.Bar, so we're ok + */ + desktop_file = g_strconcat (wmclass, ".desktop", NULL); + app = shell_app_system_lookup_heuristic_basename (system, desktop_file); + g_free (desktop_file); + + if (app) + return app; + + canonicalized = g_ascii_strdown (wmclass, -1); + + /* This handles "Fedora Eclipse", probably others. + * Note g_strdelimit is modify-in-place. */ + g_strdelimit (canonicalized, " ", '-'); + + desktop_file = g_strconcat (canonicalized, ".desktop", NULL); + + app = shell_app_system_lookup_heuristic_basename (system, desktop_file); + + g_free (canonicalized); + g_free (desktop_file); + + return app; +} + +/** + * shell_app_system_lookup_startup_wmclass: + * @system: a #ShellAppSystem + * @wmclass: (nullable): A WM_CLASS value + * + * Find a valid application whose .desktop file contains a + * StartupWMClass entry matching @wmclass. + * + * Returns: (transfer none): A #ShellApp for @wmclass + */ +ShellApp * +shell_app_system_lookup_startup_wmclass (ShellAppSystem *system, + const char *wmclass) +{ + const char *id; + + if (wmclass == NULL) + return NULL; + + id = g_hash_table_lookup (system->priv->startup_wm_class_to_id, wmclass); + if (id == NULL) + return NULL; + + return shell_app_system_lookup_app (system, id); +} + +void +_shell_app_system_notify_app_state_changed (ShellAppSystem *self, + ShellApp *app) +{ + ShellAppState state = shell_app_get_state (app); + + switch (state) + { + case SHELL_APP_STATE_RUNNING: + g_hash_table_insert (self->priv->running_apps, g_object_ref (app), NULL); + break; + case SHELL_APP_STATE_STARTING: + break; + case SHELL_APP_STATE_STOPPED: + g_hash_table_remove (self->priv->running_apps, app); + break; + default: + g_warn_if_reached(); + break; + } + g_signal_emit (self, signals[APP_STATE_CHANGED], 0, app); +} + +/** + * shell_app_system_get_running: + * @self: A #ShellAppSystem + * + * Returns the set of applications which currently have at least one + * open window. The returned list will be sorted by shell_app_compare(). + * + * Returns: (element-type ShellApp) (transfer container): Active applications + */ +GSList * +shell_app_system_get_running (ShellAppSystem *self) +{ + gpointer key, value; + GSList *ret; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, self->priv->running_apps); + + ret = NULL; + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellApp *app = key; + + ret = g_slist_prepend (ret, app); + } + + ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare); + + return ret; +} + +/** + * shell_app_system_search: + * @search_string: the search string to use + * + * Wrapper around g_desktop_app_info_search() that replaces results that + * don't validate as UTF-8 with the empty string. + * + * Returns: (array zero-terminated=1) (element-type GStrv) (transfer full): a + * list of strvs. Free each item with g_strfreev() and free the outer + * list with g_free(). + */ +char *** +shell_app_system_search (const char *search_string) +{ + char ***results = g_desktop_app_info_search (search_string); + char ***groups, **ids; + + for (groups = results; *groups; groups++) + for (ids = *groups; *ids; ids++) + if (!g_utf8_validate (*ids, -1, NULL)) + **ids = '\0'; + + return results; +} + +/** + * shell_app_system_get_installed: + * @self: the #ShellAppSystem + * + * Returns all installed apps, as a list of #GAppInfo + * + * Returns: (transfer none) (element-type GAppInfo): a list of #GAppInfo + * describing all known applications. This memory is owned by the + * #ShellAppSystem and should not be freed. + **/ +GList * +shell_app_system_get_installed (ShellAppSystem *self) +{ + return shell_app_cache_get_all (shell_app_cache_get_default ()); +} diff --git a/src/shell-app-system.h b/src/shell-app-system.h new file mode 100644 index 0000000..8719dbc --- /dev/null +++ b/src/shell-app-system.h @@ -0,0 +1,32 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_SYSTEM_H__ +#define __SHELL_APP_SYSTEM_H__ + +#include +#include +#include + +#include "shell-app.h" + +#define SHELL_TYPE_APP_SYSTEM (shell_app_system_get_type ()) +G_DECLARE_FINAL_TYPE (ShellAppSystem, shell_app_system, + SHELL, APP_SYSTEM, GObject) + +ShellAppSystem *shell_app_system_get_default (void); + +ShellApp *shell_app_system_lookup_app (ShellAppSystem *system, + const char *id); +ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *id); + +ShellApp *shell_app_system_lookup_startup_wmclass (ShellAppSystem *system, + const char *wmclass); +ShellApp *shell_app_system_lookup_desktop_wmclass (ShellAppSystem *system, + const char *wmclass); + +GSList *shell_app_system_get_running (ShellAppSystem *self); +char ***shell_app_system_search (const char *search_string); + +GList *shell_app_system_get_installed (ShellAppSystem *self); + +#endif /* __SHELL_APP_SYSTEM_H__ */ diff --git a/src/shell-app-usage.c b/src/shell-app-usage.c new file mode 100644 index 0000000..0dfb209 --- /dev/null +++ b/src/shell-app-usage.c @@ -0,0 +1,774 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shell-app-usage.h" +#include "shell-window-tracker.h" +#include "shell-global.h" + +/* This file includes modified code from + * desktop-data-engine/engine-dbus/hippo-application-monitor.c + * in the functions collecting application usage data. + * Written by Owen Taylor, originally licensed under LGPL 2.1. + * Copyright Red Hat, Inc. 2006-2008 + */ + +/** + * SECTION:shell-app-usage + * @short_description: Track application usage/state data + * + * This class maintains some usage and state statistics for + * applications by keeping track of the approximate time an application's + * windows are focused, as well as the last workspace it was seen on. + * This time tracking is implemented by watching for focus notifications, + * and computing a time delta between them. Also we watch the + * GNOME Session "StatusChanged" signal which by default is emitted after 5 + * minutes to signify idle. + */ + +#define PRIVACY_SCHEMA "org.gnome.desktop.privacy" +#define ENABLE_MONITORING_KEY "remember-app-usage" + +#define FOCUS_TIME_MIN_SECONDS 7 /* Need 7 continuous seconds of focus */ + +#define USAGE_CLEAN_DAYS 7 /* If after 7 days we haven't seen an app, purge it */ + +/* Data is saved to file SHELL_CONFIG_DIR/DATA_FILENAME */ +#define DATA_FILENAME "application_state" + +#define IDLE_TIME_TRANSITION_SECONDS 30 /* If we transition to idle, only count + * this many seconds of usage */ + +/* The ranking algorithm we use is: every time an app score reaches SCORE_MAX, + * divide all scores by 2. Scores are raised by 1 unit every SAVE_APPS_TIMEOUT + * seconds. This mechanism allows the list to update relatively fast when + * a new app is used intensively. + * To keep the list clean, and avoid being Big Brother, apps that have not been + * seen for a week and whose score is below SCORE_MIN are removed. + */ + +/* How often we save internally app data, in seconds */ +#define SAVE_APPS_TIMEOUT_SECONDS (5 * 60) + +/* With this value, an app goes from bottom to top of the + * usage list in 50 hours of use */ +#define SCORE_MAX (3600 * 50 / FOCUS_TIME_MIN_SECONDS) + +/* If an app's score in lower than this and the app has not been used in a week, + * remove it */ +#define SCORE_MIN (SCORE_MAX >> 3) + +/* http://www.gnome.org/~mccann/gnome-session/docs/gnome-session.html#org.gnome.SessionManager.Presence */ +#define GNOME_SESSION_STATUS_IDLE 3 + +typedef struct UsageData UsageData; + +struct _ShellAppUsage +{ + GObject parent; + + GFile *configfile; + GDBusProxy *session_proxy; + GSettings *privacy_settings; + guint idle_focus_change_id; + guint save_id; + gboolean currently_idle; + gboolean enable_monitoring; + + long watch_start_time; + ShellApp *watched_app; + + /* */ + GHashTable *app_usages; +}; + +G_DEFINE_TYPE (ShellAppUsage, shell_app_usage, G_TYPE_OBJECT); + +/* Represents an application record for a given context */ +struct UsageData +{ + gdouble score; /* Based on the number of times we'e seen the app and normalized */ + long last_seen; /* Used to clear old apps we've only seen a few times */ +}; + +static void shell_app_usage_finalize (GObject *object); + +static void on_session_status_changed (GDBusProxy *proxy, guint status, ShellAppUsage *self); +static void on_focus_app_changed (ShellWindowTracker *tracker, GParamSpec *spec, ShellAppUsage *self); +static void ensure_queued_save (ShellAppUsage *self); + +static gboolean idle_save_application_usage (gpointer data); + +static void restore_from_file (ShellAppUsage *self); + +static void update_enable_monitoring (ShellAppUsage *self); + +static void on_enable_monitoring_key_changed (GSettings *settings, + const gchar *key, + ShellAppUsage *self); + +static long +get_time (void) +{ + return g_get_real_time () / G_TIME_SPAN_SECOND; +} + +static void +shell_app_usage_class_init (ShellAppUsageClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_app_usage_finalize; +} + +static UsageData * +get_usage_for_app (ShellAppUsage *self, + ShellApp *app) +{ + UsageData *usage; + const char *appid = shell_app_get_id (app); + + usage = g_hash_table_lookup (self->app_usages, appid); + if (usage) + return usage; + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (self->app_usages, g_strdup (appid), usage); + + return usage; +} + +/* Limit the score to a certain level so that most used apps can change */ +static void +normalize_usage (ShellAppUsage *self) +{ + GHashTableIter iter; + UsageData *usage; + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &usage)) + usage->score /= 2; +} + +static void +increment_usage_for_app_at_time (ShellAppUsage *self, + ShellApp *app, + long time) +{ + UsageData *usage; + guint elapsed; + guint usage_count; + + usage = get_usage_for_app (self, app); + + usage->last_seen = time; + + elapsed = time - self->watch_start_time; + usage_count = elapsed / FOCUS_TIME_MIN_SECONDS; + if (usage_count > 0) + { + usage->score += usage_count; + if (usage->score > SCORE_MAX) + normalize_usage (self); + ensure_queued_save (self); + } +} + +static void +increment_usage_for_app (ShellAppUsage *self, + ShellApp *app) +{ + long curtime = get_time (); + increment_usage_for_app_at_time (self, app, curtime); +} + +static void +on_app_state_changed (ShellAppSystem *app_system, + ShellApp *app, + gpointer user_data) +{ + ShellAppUsage *self = SHELL_APP_USAGE (user_data); + UsageData *usage; + gboolean running; + + if (shell_app_is_window_backed (app)) + return; + + usage = get_usage_for_app (self, app); + + running = shell_app_get_state (app) == SHELL_APP_STATE_RUNNING; + + if (running) + usage->last_seen = get_time (); +} + +static void +on_focus_app_changed (ShellWindowTracker *tracker, + GParamSpec *spec, + ShellAppUsage *self) +{ + if (self->watched_app != NULL) + increment_usage_for_app (self, self->watched_app); + + if (self->watched_app) + g_object_unref (self->watched_app); + + g_object_get (tracker, "focus-app", &(self->watched_app), NULL); + self->watch_start_time = get_time (); +} + +static void +on_session_status_changed (GDBusProxy *proxy, + guint status, + ShellAppUsage *self) +{ + gboolean idle; + + idle = (status >= GNOME_SESSION_STATUS_IDLE); + if (self->currently_idle == idle) + return; + + self->currently_idle = idle; + if (idle) + { + long end_time; + + /* The GNOME Session signal we watch is 5 minutes, but that's a long + * time for this purpose. Instead, just add a base 30 seconds. + */ + if (self->watched_app) + { + end_time = self->watch_start_time + IDLE_TIME_TRANSITION_SECONDS; + increment_usage_for_app_at_time (self, self->watched_app, end_time); + } + } + else + { + /* Transitioning to !idle, reset the start time */ + self->watch_start_time = get_time (); + } +} + +static void +session_proxy_signal (GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data) +{ + if (g_str_equal (signal_name, "StatusChanged")) + { + guint status; + g_variant_get (parameters, "(u)", &status); + on_session_status_changed (proxy, status, SHELL_APP_USAGE (user_data)); + } +} + +static void +shell_app_usage_init (ShellAppUsage *self) +{ + ShellGlobal *global; + char *shell_userdata_dir, *path; + GDBusConnection *session_bus; + ShellWindowTracker *tracker; + ShellAppSystem *app_system; + + global = shell_global_get (); + + self->app_usages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + + tracker = shell_window_tracker_get_default (); + g_signal_connect (tracker, "notify::focus-app", G_CALLBACK (on_focus_app_changed), self); + + app_system = shell_app_system_get_default (); + g_signal_connect (app_system, "app-state-changed", G_CALLBACK (on_app_state_changed), self); + + session_bus = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + self->session_proxy = g_dbus_proxy_new_sync (session_bus, + G_DBUS_PROXY_FLAGS_NONE, + NULL, /* interface info */ + "org.gnome.SessionManager", + "/org/gnome/SessionManager/Presence", + "org.gnome.SessionManager", + NULL, /* cancellable */ + NULL /* error */); + g_signal_connect (self->session_proxy, "g-signal", G_CALLBACK (session_proxy_signal), self); + g_object_unref (session_bus); + + self->currently_idle = FALSE; + self->enable_monitoring = FALSE; + + g_object_get (global, "userdatadir", &shell_userdata_dir, NULL), + path = g_build_filename (shell_userdata_dir, DATA_FILENAME, NULL); + g_free (shell_userdata_dir); + self->configfile = g_file_new_for_path (path); + g_free (path); + restore_from_file (self); + + self->privacy_settings = g_settings_new(PRIVACY_SCHEMA); + g_signal_connect (self->privacy_settings, + "changed::" ENABLE_MONITORING_KEY, + G_CALLBACK (on_enable_monitoring_key_changed), + self); + update_enable_monitoring (self); +} + +static void +shell_app_usage_finalize (GObject *object) +{ + ShellAppUsage *self = SHELL_APP_USAGE (object); + + g_clear_handle_id (&self->save_id, g_source_remove); + + g_object_unref (self->privacy_settings); + + g_object_unref (self->configfile); + + g_object_unref (self->session_proxy); + + G_OBJECT_CLASS (shell_app_usage_parent_class)->finalize(object); +} + +static int +sort_apps_by_usage (gconstpointer a, + gconstpointer b, + gpointer datap) +{ + ShellAppUsage *self = datap; + ShellApp *app_a, *app_b; + UsageData *usage_a, *usage_b; + + app_a = (ShellApp*)a; + app_b = (ShellApp*)b; + + usage_a = g_hash_table_lookup (self->app_usages, shell_app_get_id (app_a)); + usage_b = g_hash_table_lookup (self->app_usages, shell_app_get_id (app_b)); + + return usage_b->score - usage_a->score; +} + +/** + * shell_app_usage_get_most_used: + * @usage: the usage instance to request + * + * Returns: (element-type ShellApp) (transfer full): List of applications + */ +GSList * +shell_app_usage_get_most_used (ShellAppUsage *self) +{ + GSList *apps; + char *appid; + ShellAppSystem *appsys; + GHashTableIter iter; + + appsys = shell_app_system_get_default (); + + g_hash_table_iter_init (&iter, self->app_usages); + apps = NULL; + while (g_hash_table_iter_next (&iter, (gpointer *) &appid, NULL)) + { + ShellApp *app; + + app = shell_app_system_lookup_app (appsys, appid); + if (!app) + continue; + + apps = g_slist_prepend (apps, g_object_ref (app)); + } + + apps = g_slist_sort_with_data (apps, sort_apps_by_usage, self); + + return apps; +} + + +/** + * shell_app_usage_compare: + * @self: the usage instance to request + * @id_a: ID of first app + * @id_b: ID of second app + * + * Compare @id_a and @id_b based on frequency of use. + * + * Returns: -1 if @id_a ranks higher than @id_b, 1 if @id_b ranks higher + * than @id_a, and 0 if both rank equally. + */ +int +shell_app_usage_compare (ShellAppUsage *self, + const char *id_a, + const char *id_b) +{ + UsageData *usage_a, *usage_b; + + usage_a = g_hash_table_lookup (self->app_usages, id_a); + usage_b = g_hash_table_lookup (self->app_usages, id_b); + + if (usage_a == NULL && usage_b == NULL) + return 0; + else if (usage_a == NULL) + return 1; + else if (usage_b == NULL) + return -1; + + return usage_b->score - usage_a->score; +} + +static void +ensure_queued_save (ShellAppUsage *self) +{ + if (self->save_id != 0) + return; + self->save_id = g_timeout_add_seconds (SAVE_APPS_TIMEOUT_SECONDS, idle_save_application_usage, self); + g_source_set_name_by_id (self->save_id, "[gnome-shell] idle_save_application_usage"); +} + +/* Clean up apps we see rarely. + * The logic behind this is that if an app was seen less than SCORE_MIN times + * and not seen for a week, it can probably be forgotten about. + * This should much reduce the size of the list and avoid 'pollution'. */ +static gboolean +idle_clean_usage (ShellAppUsage *self) +{ + GHashTableIter iter; + UsageData *usage; + long current_time; + long week_ago; + + current_time = get_time (); + week_ago = current_time - (7 * 24 * 60 * 60); + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, NULL, (gpointer *) &usage)) + { + if ((usage->score < SCORE_MIN) && + (usage->last_seen < week_ago)) + g_hash_table_iter_remove (&iter); + } + + return FALSE; +} + +static gboolean +write_escaped (GDataOutputStream *stream, + const char *str, + GError **error) +{ + gboolean ret; + char *quoted = g_markup_escape_text (str, -1); + ret = g_data_output_stream_put_string (stream, quoted, NULL, error); + g_free (quoted); + return ret; +} + +static gboolean +write_attribute_string (GDataOutputStream *stream, + const char *elt_name, + const char *str, + GError **error) +{ + gboolean ret = FALSE; + char *elt; + + elt = g_strdup_printf (" %s=\"", elt_name); + ret = g_data_output_stream_put_string (stream, elt, NULL, error); + g_free (elt); + if (!ret) + goto out; + + ret = write_escaped (stream, str, error); + if (!ret) + goto out; + + ret = g_data_output_stream_put_string (stream, "\"", NULL, error); + +out: + return ret; +} + +static gboolean +write_attribute_uint (GDataOutputStream *stream, + const char *elt_name, + guint value, + GError **error) +{ + gboolean ret; + char *buf; + + buf = g_strdup_printf ("%u", value); + ret = write_attribute_string (stream, elt_name, buf, error); + g_free (buf); + + return ret; +} + +static gboolean +write_attribute_double (GDataOutputStream *stream, + const char *elt_name, + double value, + GError **error) +{ + gchar buf[G_ASCII_DTOSTR_BUF_SIZE]; + gboolean ret; + + g_ascii_dtostr (buf, sizeof (buf), value); + ret = write_attribute_string (stream, elt_name, buf, error); + + return ret; +} + +/* Save app data lists to file */ +static gboolean +idle_save_application_usage (gpointer data) +{ + ShellAppUsage *self = SHELL_APP_USAGE (data); + char *id; + GHashTableIter iter; + UsageData *usage; + GFileOutputStream *output; + GOutputStream *buffered_output; + GDataOutputStream *data_output; + GError *error = NULL; + + self->save_id = 0; + + /* Parent directory is already created by shell-global */ + output = g_file_replace (self->configfile, NULL, FALSE, G_FILE_CREATE_NONE, NULL, &error); + if (!output) + { + g_debug ("Could not save applications usage data: %s", error->message); + g_error_free (error); + return FALSE; + } + buffered_output = g_buffered_output_stream_new (G_OUTPUT_STREAM (output)); + g_object_unref (output); + data_output = g_data_output_stream_new (G_OUTPUT_STREAM (buffered_output)); + g_object_unref (buffered_output); + + if (!g_data_output_stream_put_string (data_output, "\n\n", NULL, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, " \n", NULL, &error)) + goto out; + + g_hash_table_iter_init (&iter, self->app_usages); + + while (g_hash_table_iter_next (&iter, (gpointer *) &id, (gpointer *) &usage)) + { + ShellApp *app; + + app = shell_app_system_lookup_app (shell_app_system_get_default(), id); + + if (!app) + continue; + + if (!g_data_output_stream_put_string (data_output, " score, &error)) + goto out; + if (!write_attribute_uint (data_output, "last-seen", usage->last_seen, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, "/>\n", NULL, &error)) + goto out; + } + if (!g_data_output_stream_put_string (data_output, " \n", NULL, &error)) + goto out; + if (!g_data_output_stream_put_string (data_output, "\n", NULL, &error)) + goto out; + +out: + if (!error) + g_output_stream_close_async (G_OUTPUT_STREAM (data_output), 0, NULL, NULL, NULL); + g_object_unref (data_output); + if (error) + { + g_debug ("Could not save applications usage data: %s", error->message); + g_error_free (error); + } + return FALSE; +} + +static void +shell_app_usage_start_element_handler (GMarkupParseContext *context, + const gchar *element_name, + const gchar **attribute_names, + const gchar **attribute_values, + gpointer user_data, + GError **error) +{ + ShellAppUsage *self = user_data; + + if (strcmp (element_name, "application-state") == 0) + { + } + else if (strcmp (element_name, "context") == 0) + { + } + else if (strcmp (element_name, "application") == 0) + { + const char **attribute; + const char **value; + UsageData *usage; + char *appid = NULL; + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "id") == 0) + { + appid = g_strdup (*value); + break; + } + } + + if (!appid) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Missing attribute id on <%s> element", + element_name); + return; + } + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (self->app_usages, appid, usage); + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "score") == 0) + { + usage->score = g_ascii_strtod (*value, NULL); + } + else if (strcmp (*attribute, "last-seen") == 0) + { + usage->last_seen = (guint) g_ascii_strtoull (*value, NULL, 10); + } + } + } + else + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Unknown element <%s>", + element_name); + } +} + +static GMarkupParser app_state_parse_funcs = +{ + shell_app_usage_start_element_handler, + NULL, + NULL, + NULL, + NULL +}; + +/* Load data about apps usage from file */ +static void +restore_from_file (ShellAppUsage *self) +{ + GFileInputStream *input; + GMarkupParseContext *parse_context; + GError *error = NULL; + char buf[1024]; + + input = g_file_read (self->configfile, NULL, &error); + if (error) + { + if (error->code != G_IO_ERROR_NOT_FOUND) + g_warning ("Could not load applications usage data: %s", error->message); + + g_error_free (error); + return; + } + + parse_context = g_markup_parse_context_new (&app_state_parse_funcs, 0, self, NULL); + + while (TRUE) + { + gssize count = g_input_stream_read ((GInputStream*) input, buf, sizeof(buf), NULL, &error); + if (count <= 0) + goto out; + if (!g_markup_parse_context_parse (parse_context, buf, count, &error)) + goto out; + } + +out: + g_markup_parse_context_free (parse_context); + g_input_stream_close ((GInputStream*)input, NULL, NULL); + g_object_unref (input); + + idle_clean_usage (self); + + if (error) + { + g_warning ("Could not load applications usage data: %s", error->message); + g_error_free (error); + } +} + +/* Enable or disable the timers, depending on the value of ENABLE_MONITORING_KEY + * and taking care of the previous state. If selfing is disabled, we still + * report apps usage based on (possibly) saved data, but don't collect data. + */ +static void +update_enable_monitoring (ShellAppUsage *self) +{ + gboolean enable; + + enable = g_settings_get_boolean (self->privacy_settings, + ENABLE_MONITORING_KEY); + + /* Be sure not to start the timers if they were already set */ + if (enable && !self->enable_monitoring) + { + on_focus_app_changed (shell_window_tracker_get_default (), NULL, self); + } + /* ...and don't try to stop them if they were not running */ + else if (!enable && self->enable_monitoring) + { + if (self->watched_app) + g_object_unref (self->watched_app); + self->watched_app = NULL; + g_clear_handle_id (&self->save_id, g_source_remove); + } + + self->enable_monitoring = enable; +} + +/* Called when the ENABLE_MONITORING_KEY boolean has changed */ +static void +on_enable_monitoring_key_changed (GSettings *settings, + const gchar *key, + ShellAppUsage *self) +{ + update_enable_monitoring (self); +} + +/** + * shell_app_usage_get_default: + * + * Return Value: (transfer none): The global #ShellAppUsage instance + */ +ShellAppUsage * +shell_app_usage_get_default (void) +{ + static ShellAppUsage *instance; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_APP_USAGE, NULL); + + return instance; +} diff --git a/src/shell-app-usage.h b/src/shell-app-usage.h new file mode 100644 index 0000000..4b0e169 --- /dev/null +++ b/src/shell-app-usage.h @@ -0,0 +1,23 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_USAGE_H__ +#define __SHELL_APP_USAGE_H__ + +#include "shell-app.h" +#include "shell-window-tracker.h" + +G_BEGIN_DECLS + +#define SHELL_TYPE_APP_USAGE (shell_app_usage_get_type ()) +G_DECLARE_FINAL_TYPE (ShellAppUsage, shell_app_usage, + SHELL, APP_USAGE, GObject) + +ShellAppUsage* shell_app_usage_get_default(void); + +GSList *shell_app_usage_get_most_used (ShellAppUsage *usage); +int shell_app_usage_compare (ShellAppUsage *self, + const char *id_a, + const char *id_b); + +G_END_DECLS + +#endif /* __SHELL_APP_USAGE_H__ */ diff --git a/src/shell-app.c b/src/shell-app.c new file mode 100644 index 0000000..5c38a9c --- /dev/null +++ b/src/shell-app.c @@ -0,0 +1,1756 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include + +#include + +#include +#include +#include +#include + +#include "shell-app-private.h" +#include "shell-enum-types.h" +#include "shell-global.h" +#include "shell-util.h" +#include "shell-app-system-private.h" +#include "shell-window-tracker-private.h" +#include "st.h" +#include "gtkactionmuxer.h" +#include "org-gtk-application.h" +#include "switcheroo-control.h" + +#ifdef HAVE_SYSTEMD +#include +#include +#include +#endif + +/* This is mainly a memory usage optimization - the user is going to + * be running far fewer of the applications at one time than they have + * installed. But it also just helps keep the code more logically + * separated. + */ +typedef struct { + guint refcount; + + /* Signal connection to dirty window sort list on workspace changes */ + gulong workspace_switch_id; + + GSList *windows; + + guint interesting_windows; + + /* Whether or not we need to resort the windows; this is done on demand */ + guint window_sort_stale : 1; + + /* See GApplication documentation */ + GtkActionMuxer *muxer; + char *unique_bus_name; + GDBusConnection *session; + + /* GDBus Proxy for getting application busy state */ + ShellOrgGtkApplication *application_proxy; + GCancellable *cancellable; + +} ShellAppRunningState; + +/** + * SECTION:shell-app + * @short_description: Object representing an application + * + * This object wraps a #GDesktopAppInfo, providing methods and signals + * primarily useful for running applications. + */ +struct _ShellApp +{ + GObject parent; + + int started_on_workspace; + + ShellAppState state; + + GDesktopAppInfo *info; /* If NULL, this app is backed by one or more + * MetaWindow. For purposes of app title + * etc., we use the first window added, + * because it's most likely to be what we + * want (e.g. it will be of TYPE_NORMAL from + * the way shell-window-tracker.c works). + */ + GIcon *fallback_icon; + MetaWindow *fallback_icon_window; + + ShellAppRunningState *running_state; + + char *window_id_string; + char *name_collation_key; +}; + +enum { + PROP_0, + + PROP_STATE, + PROP_BUSY, + PROP_ID, + PROP_ACTION_GROUP, + PROP_ICON, + PROP_APP_INFO, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +enum { + WINDOWS_CHANGED, + LAST_SIGNAL +}; + +static guint shell_app_signals[LAST_SIGNAL] = { 0 }; + +static void create_running_state (ShellApp *app); +static void unref_running_state (ShellAppRunningState *state); + +G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT) + +static void +shell_app_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellApp *app = SHELL_APP (gobject); + + switch (prop_id) + { + case PROP_STATE: + g_value_set_enum (value, app->state); + break; + case PROP_BUSY: + g_value_set_boolean (value, shell_app_get_busy (app)); + break; + case PROP_ID: + g_value_set_string (value, shell_app_get_id (app)); + break; + case PROP_ICON: + g_value_set_object (value, shell_app_get_icon (app)); + break; + case PROP_ACTION_GROUP: + if (app->running_state) + g_value_set_object (value, app->running_state->muxer); + break; + case PROP_APP_INFO: + if (app->info) + g_value_set_object (value, app->info); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_app_set_property (GObject *gobject, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellApp *app = SHELL_APP (gobject); + + switch (prop_id) + { + case PROP_APP_INFO: + _shell_app_set_app_info (app, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +const char * +shell_app_get_id (ShellApp *app) +{ + if (app->info) + return g_app_info_get_id (G_APP_INFO (app->info)); + return app->window_id_string; +} + +static MetaWindow * +window_backed_app_get_window (ShellApp *app) +{ + g_assert (app->info == NULL); + if (app->running_state) + { + g_assert (app->running_state->windows); + return app->running_state->windows->data; + } + else + return NULL; +} + +static GIcon * +x11_window_create_fallback_gicon (MetaWindow *window) +{ + StTextureCache *texture_cache; + cairo_surface_t *surface; + + g_object_get (window, "icon", &surface, NULL); + + texture_cache = st_texture_cache_get_default (); + return st_texture_cache_load_cairo_surface_to_gicon (texture_cache, surface); +} + +static void +on_window_icon_changed (GObject *object, + const GParamSpec *pspec, + gpointer user_data) +{ + MetaWindow *window = META_WINDOW (object); + ShellApp *app = user_data; + + g_clear_object (&app->fallback_icon); + app->fallback_icon = x11_window_create_fallback_gicon (window); + + if (!app->fallback_icon) + app->fallback_icon = g_themed_icon_new ("application-x-executable"); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ICON]); +} + +/** + * shell_app_get_icon: + * + * Look up the icon for this application + * + * Return value: (transfer none): A #GIcon + */ +GIcon * +shell_app_get_icon (ShellApp *app) +{ + MetaWindow *window = NULL; + + g_return_val_if_fail (SHELL_IS_APP (app), NULL); + + if (app->info) + return g_app_info_get_icon (G_APP_INFO (app->info)); + + if (app->fallback_icon) + return app->fallback_icon; + + /* During a state transition from running to not-running for + * window-backend apps, it's possible we get a request for the icon. + * Avoid asserting here and just return a fallback icon + */ + if (app->running_state != NULL) + window = window_backed_app_get_window (app); + + if (window && + meta_window_get_client_type (window) == META_WINDOW_CLIENT_TYPE_X11) + { + app->fallback_icon_window = window; + app->fallback_icon = x11_window_create_fallback_gicon (window); + g_signal_connect (G_OBJECT (window), + "notify::icon", G_CALLBACK (on_window_icon_changed), app); + } + else + { + app->fallback_icon = g_themed_icon_new ("application-x-executable"); + } + + return app->fallback_icon; +} + +/** + * shell_app_create_icon_texture: + * + * Look up the icon for this application, and create a #ClutterActor + * for it at the given size. + * + * Return value: (transfer none): A floating #ClutterActor + */ +ClutterActor * +shell_app_create_icon_texture (ShellApp *app, + int size) +{ + ClutterActor *ret; + + ret = st_icon_new (); + st_icon_set_icon_size (ST_ICON (ret), size); + st_icon_set_fallback_icon_name (ST_ICON (ret), "application-x-executable"); + + g_object_bind_property (app, "icon", ret, "gicon", G_BINDING_SYNC_CREATE); + + if (shell_app_is_window_backed (app)) + st_widget_add_style_class_name (ST_WIDGET (ret), "fallback-app-icon"); + + return ret; +} + +const char * +shell_app_get_name (ShellApp *app) +{ + if (app->info) + return g_app_info_get_name (G_APP_INFO (app->info)); + else + { + MetaWindow *window = window_backed_app_get_window (app); + const char *name = NULL; + + if (window) + name = meta_window_get_wm_class (window); + if (!name) + name = C_("program", "Unknown"); + return name; + } +} + +const char * +shell_app_get_description (ShellApp *app) +{ + if (app->info) + return g_app_info_get_description (G_APP_INFO (app->info)); + else + return NULL; +} + +/** + * shell_app_is_window_backed: + * + * A window backed application is one which represents just an open + * window, i.e. there's no .desktop file association, so we don't know + * how to launch it again. + */ +gboolean +shell_app_is_window_backed (ShellApp *app) +{ + return app->info == NULL; +} + +typedef struct { + MetaWorkspace *workspace; + GSList **transients; +} CollectTransientsData; + +static gboolean +collect_transients_on_workspace (MetaWindow *window, + gpointer datap) +{ + CollectTransientsData *data = datap; + + if (data->workspace && meta_window_get_workspace (window) != data->workspace) + return TRUE; + + *data->transients = g_slist_prepend (*data->transients, window); + return TRUE; +} + +/* The basic idea here is that when we're targeting a window, + * if it has transients we want to pick the most recent one + * the user interacted with. + * This function makes raising GEdit with the file chooser + * open work correctly. + */ +static MetaWindow * +find_most_recent_transient_on_same_workspace (MetaDisplay *display, + MetaWindow *reference) +{ + GSList *transients, *transients_sorted, *iter; + MetaWindow *result; + CollectTransientsData data; + + transients = NULL; + data.workspace = meta_window_get_workspace (reference); + data.transients = &transients; + + meta_window_foreach_transient (reference, collect_transients_on_workspace, &data); + + transients_sorted = meta_display_sort_windows_by_stacking (display, transients); + /* Reverse this so we're top-to-bottom (yes, we should probably change the order + * returned from the sort_windows_by_stacking function) + */ + transients_sorted = g_slist_reverse (transients_sorted); + g_slist_free (transients); + transients = NULL; + + result = NULL; + for (iter = transients_sorted; iter; iter = iter->next) + { + MetaWindow *window = iter->data; + MetaWindowType wintype = meta_window_get_window_type (window); + + /* Don't want to focus UTILITY types, like the Gimp toolbars */ + if (wintype == META_WINDOW_NORMAL || + wintype == META_WINDOW_DIALOG) + { + result = window; + break; + } + } + g_slist_free (transients_sorted); + return result; +} + +static MetaWorkspace * +get_active_workspace (void) +{ + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + return meta_workspace_manager_get_active_workspace (workspace_manager); +} + +/** + * shell_app_activate_window: + * @app: a #ShellApp + * @window: (nullable): Window to be focused + * @timestamp: Event timestamp + * + * Bring all windows for the given app to the foreground, + * but ensure that @window is on top. If @window is %NULL, + * the window with the most recent user time for the app + * will be used. + * + * This function has no effect if @app is not currently running. + */ +void +shell_app_activate_window (ShellApp *app, + MetaWindow *window, + guint32 timestamp) +{ + g_autoptr (GSList) windows = NULL; + + if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING) + return; + + windows = shell_app_get_windows (app); + if (window == NULL && windows) + window = windows->data; + + if (!g_slist_find (windows, window)) + return; + else + { + GSList *windows_reversed, *iter; + ShellGlobal *global = shell_global_get (); + MetaDisplay *display = shell_global_get_display (global); + MetaWorkspace *active = get_active_workspace (); + MetaWorkspace *workspace = meta_window_get_workspace (window); + guint32 last_user_timestamp = meta_display_get_last_user_time (display); + MetaWindow *most_recent_transient; + + if (meta_display_xserver_time_is_before (display, timestamp, last_user_timestamp)) + { + meta_window_set_demands_attention (window); + return; + } + + /* Now raise all the other windows for the app that are on + * the same workspace, in reverse order to preserve the stacking. + */ + windows_reversed = g_slist_copy (windows); + windows_reversed = g_slist_reverse (windows_reversed); + for (iter = windows_reversed; iter; iter = iter->next) + { + MetaWindow *other_window = iter->data; + + if (other_window != window && meta_window_get_workspace (other_window) == workspace) + meta_window_raise (other_window); + } + g_slist_free (windows_reversed); + + /* If we have a transient that the user's interacted with more recently than + * the window, pick that. + */ + most_recent_transient = find_most_recent_transient_on_same_workspace (display, window); + if (most_recent_transient + && meta_display_xserver_time_is_before (display, + meta_window_get_user_time (window), + meta_window_get_user_time (most_recent_transient))) + window = most_recent_transient; + + + if (active != workspace) + meta_workspace_activate_with_focus (workspace, window, timestamp); + else + meta_window_activate (window, timestamp); + } +} + + +void +shell_app_update_window_actions (ShellApp *app, MetaWindow *window) +{ + const char *object_path; + + object_path = meta_window_get_gtk_window_object_path (window); + if (object_path != NULL) + { + GActionGroup *actions; + + actions = g_object_get_data (G_OBJECT (window), "actions"); + if (actions == NULL) + { + actions = G_ACTION_GROUP (g_dbus_action_group_get (app->running_state->session, + meta_window_get_gtk_unique_bus_name (window), + object_path)); + g_object_set_data_full (G_OBJECT (window), "actions", actions, g_object_unref); + } + + g_assert (app->running_state->muxer); + gtk_action_muxer_insert (app->running_state->muxer, "win", actions); + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ACTION_GROUP]); + } +} + +/** + * shell_app_activate: + * @app: a #ShellApp + * + * Like shell_app_activate_full(), but using the default workspace and + * event timestamp. + */ +void +shell_app_activate (ShellApp *app) +{ + return shell_app_activate_full (app, -1, 0); +} + +/** + * shell_app_activate_full: + * @app: a #ShellApp + * @workspace: launch on this workspace, or -1 for default. Ignored if + * activating an existing window + * @timestamp: Event timestamp + * + * Perform an appropriate default action for operating on this application, + * dependent on its current state. For example, if the application is not + * currently running, launch it. If it is running, activate the most + * recently used NORMAL window (or if that window has a transient, the most + * recently used transient for that window). + */ +void +shell_app_activate_full (ShellApp *app, + int workspace, + guint32 timestamp) +{ + ShellGlobal *global; + + global = shell_global_get (); + + if (timestamp == 0) + timestamp = shell_global_get_current_time (global); + + switch (app->state) + { + case SHELL_APP_STATE_STOPPED: + { + GError *error = NULL; + if (!shell_app_launch (app, timestamp, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, &error)) + { + char *msg; + msg = g_strdup_printf (_("Failed to launch “%s”"), shell_app_get_name (app)); + shell_global_notify_error (global, + msg, + error->message); + g_free (msg); + g_clear_error (&error); + } + } + break; + case SHELL_APP_STATE_STARTING: + break; + case SHELL_APP_STATE_RUNNING: + shell_app_activate_window (app, NULL, timestamp); + break; + default: + g_assert_not_reached(); + break; + } +} + +/** + * shell_app_open_new_window: + * @app: a #ShellApp + * @workspace: open on this workspace, or -1 for default + * + * Request that the application create a new window. + */ +void +shell_app_open_new_window (ShellApp *app, + int workspace) +{ + GActionGroup *group = NULL; + const char * const *actions; + + g_return_if_fail (app->info != NULL); + + /* First check whether the application provides a "new-window" desktop + * action - it is a safe bet that it will open a new window, and activating + * it will trigger startup notification if necessary + */ + actions = g_desktop_app_info_list_actions (G_DESKTOP_APP_INFO (app->info)); + + if (g_strv_contains (actions, "new-window")) + { + shell_app_launch_action (app, "new-window", 0, workspace); + return; + } + + /* Next, check whether the app exports an explicit "new-window" action + * that we can activate on the bus - the muxer will add startup notification + * information to the platform data, so this should work just as well as + * desktop actions. + */ + group = app->running_state ? G_ACTION_GROUP (app->running_state->muxer) + : NULL; + + if (group && + g_action_group_has_action (group, "app.new-window") && + g_action_group_get_action_parameter_type (group, "app.new-window") == NULL) + { + g_action_group_activate_action (group, "app.new-window", NULL); + + return; + } + + /* Lastly, just always launch the application again, even if we know + * it was already running. For most applications this + * should have the effect of creating a new window, whether that's + * a second process (in the case of Calculator) or IPC to existing + * instance (Firefox). There are a few less-sensical cases such + * as say Pidgin. + */ + shell_app_launch (app, 0, workspace, SHELL_APP_LAUNCH_GPU_APP_PREF, NULL); +} + +/** + * shell_app_can_open_new_window: + * @app: a #ShellApp + * + * Returns %TRUE if the app supports opening a new window through + * shell_app_open_new_window() (ie, if calling that function will + * result in actually opening a new window and not something else, + * like presenting the most recently active one) + */ +gboolean +shell_app_can_open_new_window (ShellApp *app) +{ + ShellAppRunningState *state; + MetaWindow *window; + GDesktopAppInfo *desktop_info; + const char * const *desktop_actions; + + /* Apps that are stopped can always open new windows, because + * activating them would open the first one; if they are starting, + * we cannot tell whether they can open additional windows until + * they are running */ + if (app->state != SHELL_APP_STATE_RUNNING) + return app->state == SHELL_APP_STATE_STOPPED; + + state = app->running_state; + + /* If the app has an explicit new-window action, then it can + (or it should be able to) ... + */ + if (g_action_group_has_action (G_ACTION_GROUP (state->muxer), "app.new-window")) + return TRUE; + + /* If the app doesn't have a desktop file, then nothing is possible */ + if (!app->info) + return FALSE; + + desktop_info = G_DESKTOP_APP_INFO (app->info); + + /* If the app is explicitly telling us via its desktop file, then we know + * for sure + */ + if (g_desktop_app_info_has_key (desktop_info, "SingleMainWindow")) + return !g_desktop_app_info_get_boolean (desktop_info, + "SingleMainWindow"); + + /* GNOME-specific key, for backwards compatibility with apps that haven't + * started using the XDG "SingleMainWindow" key yet + */ + if (g_desktop_app_info_has_key (desktop_info, "X-GNOME-SingleWindow")) + return !g_desktop_app_info_get_boolean (desktop_info, + "X-GNOME-SingleWindow"); + + /* If it has a new-window desktop action, it should be able to */ + desktop_actions = g_desktop_app_info_list_actions (desktop_info); + if (desktop_actions && g_strv_contains (desktop_actions, "new-window")) + return TRUE; + + /* If this is a unique GtkApplication, and we don't have a new-window, then + probably we can't + + We don't consider non-unique GtkApplications here to handle cases like + evince, which don't export a new-window action because each window is in + a different process. In any case, in a non-unique GtkApplication each + Activate() knows nothing about the other instances, so it will show a + new window. + */ + + window = state->windows->data; + + if (state->unique_bus_name != NULL && + meta_window_get_gtk_application_object_path (window) != NULL) + { + if (meta_window_get_gtk_application_id (window) != NULL) + return FALSE; + else + return TRUE; + } + + /* In all other cases, we don't have a reliable source of information + or a decent heuristic, so we err on the compatibility side and say + yes. + */ + return TRUE; +} + +/** + * shell_app_get_state: + * @app: a #ShellApp + * + * Returns: State of the application + */ +ShellAppState +shell_app_get_state (ShellApp *app) +{ + return app->state; +} + +typedef struct { + ShellApp *app; + MetaWorkspace *active_workspace; +} CompareWindowsData; + +static int +shell_app_compare_windows (gconstpointer a, + gconstpointer b, + gpointer datap) +{ + MetaWindow *win_a = (gpointer)a; + MetaWindow *win_b = (gpointer)b; + CompareWindowsData *data = datap; + gboolean ws_a, ws_b; + gboolean vis_a, vis_b; + + ws_a = meta_window_get_workspace (win_a) == data->active_workspace; + ws_b = meta_window_get_workspace (win_b) == data->active_workspace; + + if (ws_a && !ws_b) + return -1; + else if (!ws_a && ws_b) + return 1; + + vis_a = meta_window_showing_on_its_workspace (win_a); + vis_b = meta_window_showing_on_its_workspace (win_b); + + if (vis_a && !vis_b) + return -1; + else if (!vis_a && vis_b) + return 1; + + return meta_window_get_user_time (win_b) - meta_window_get_user_time (win_a); +} + +/** + * shell_app_get_windows: + * @app: + * + * Get the windows which are associated with this application. The + * returned list will be sorted first by whether they're on the + * active workspace, then by whether they're visible, and finally + * by the time the user last interacted with them. + * + * Returns: (transfer container) (element-type MetaWindow): List of windows + */ +GSList * +shell_app_get_windows (ShellApp *app) +{ + GSList *windows = NULL; + GSList *l; + + if (app->running_state == NULL) + return NULL; + + if (app->running_state->window_sort_stale) + { + CompareWindowsData data; + data.app = app; + data.active_workspace = get_active_workspace (); + app->running_state->windows = g_slist_sort_with_data (app->running_state->windows, shell_app_compare_windows, &data); + app->running_state->window_sort_stale = FALSE; + } + + for (l = app->running_state->windows; l; l = l->next) + if (!meta_window_is_override_redirect (META_WINDOW (l->data))) + windows = g_slist_prepend (windows, l->data); + + return g_slist_reverse (windows); +} + +guint +shell_app_get_n_windows (ShellApp *app) +{ + if (app->running_state == NULL) + return 0; + return g_slist_length (app->running_state->windows); +} + +gboolean +shell_app_is_on_workspace (ShellApp *app, + MetaWorkspace *workspace) +{ + GSList *iter; + + if (shell_app_get_state (app) == SHELL_APP_STATE_STARTING) + { + if (app->started_on_workspace == -1 || + meta_workspace_index (workspace) == app->started_on_workspace) + return TRUE; + else + return FALSE; + } + + if (app->running_state == NULL) + return FALSE; + + for (iter = app->running_state->windows; iter; iter = iter->next) + { + if (meta_window_get_workspace (iter->data) == workspace) + return TRUE; + } + + return FALSE; +} + +static int +shell_app_get_last_user_time (ShellApp *app) +{ + GSList *iter; + guint32 last_user_time; + + last_user_time = 0; + + if (app->running_state != NULL) + { + for (iter = app->running_state->windows; iter; iter = iter->next) + last_user_time = MAX (last_user_time, meta_window_get_user_time (iter->data)); + } + + return (int)last_user_time; +} + +static gboolean +shell_app_is_minimized (ShellApp *app) +{ + GSList *iter; + + if (app->running_state == NULL) + return FALSE; + + for (iter = app->running_state->windows; iter; iter = iter->next) + { + if (meta_window_showing_on_its_workspace (iter->data)) + return FALSE; + } + + return TRUE; +} + +/** + * shell_app_compare: + * @app: + * @other: A #ShellApp + * + * Compare one #ShellApp instance to another, in the following way: + * - Running applications sort before not-running applications. + * - If one of them has non-minimized windows and the other does not, + * the one with visible windows is first. + * - Finally, the application which the user interacted with most recently + * compares earlier. + */ +int +shell_app_compare (ShellApp *app, + ShellApp *other) +{ + gboolean min_app, min_other; + + if (app->state != other->state) + { + if (app->state == SHELL_APP_STATE_RUNNING) + return -1; + return 1; + } + + min_app = shell_app_is_minimized (app); + min_other = shell_app_is_minimized (other); + + if (min_app != min_other) + { + if (min_other) + return -1; + return 1; + } + + if (app->state == SHELL_APP_STATE_RUNNING) + { + if (app->running_state->windows && !other->running_state->windows) + return -1; + else if (!app->running_state->windows && other->running_state->windows) + return 1; + + return shell_app_get_last_user_time (other) - shell_app_get_last_user_time (app); + } + + return 0; +} + +ShellApp * +_shell_app_new_for_window (MetaWindow *window) +{ + ShellApp *app; + + app = g_object_new (SHELL_TYPE_APP, NULL); + + app->window_id_string = g_strdup_printf ("window:%d", meta_window_get_stable_sequence (window)); + + _shell_app_add_window (app, window); + + return app; +} + +ShellApp * +_shell_app_new (GDesktopAppInfo *info) +{ + ShellApp *app; + + app = g_object_new (SHELL_TYPE_APP, + "app-info", info, + NULL); + + return app; +} + +void +_shell_app_set_app_info (ShellApp *app, + GDesktopAppInfo *info) +{ + g_set_object (&app->info, info); + + g_clear_pointer (&app->name_collation_key, g_free); + if (app->info) + app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1); +} + +static void +shell_app_state_transition (ShellApp *app, + ShellAppState state) +{ + if (app->state == state) + return; + g_return_if_fail (!(app->state == SHELL_APP_STATE_RUNNING && + state == SHELL_APP_STATE_STARTING)); + app->state = state; + + _shell_app_system_notify_app_state_changed (shell_app_system_get_default (), app); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_STATE]); +} + +static void +shell_app_on_user_time_changed (MetaWindow *window, + GParamSpec *pspec, + ShellApp *app) +{ + g_assert (app->running_state != NULL); + + /* Ideally we don't want to emit windows-changed if the sort order + * isn't actually changing. This check catches most of those. + */ + if (window != app->running_state->windows->data) + { + app->running_state->window_sort_stale = TRUE; + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); + } +} + +static void +shell_app_sync_running_state (ShellApp *app) +{ + g_return_if_fail (app->running_state != NULL); + + if (app->state != SHELL_APP_STATE_STARTING) + { + if (app->running_state->interesting_windows == 0) + shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); + else + shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); + } +} + + +static void +shell_app_on_skip_taskbar_changed (MetaWindow *window, + GParamSpec *pspec, + ShellApp *app) +{ + g_assert (app->running_state != NULL); + + /* we rely on MetaWindow:skip-taskbar only being notified + * when it actually changes; when that assumption breaks, + * we'll have to track the "interesting" windows themselves + */ + if (meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows--; + else + app->running_state->interesting_windows++; + + shell_app_sync_running_state (app); +} + +static void +shell_app_on_ws_switch (MetaWorkspaceManager *workspace_manager, + int from, + int to, + MetaMotionDirection direction, + gpointer data) +{ + ShellApp *app = SHELL_APP (data); + + g_assert (app->running_state != NULL); + + app->running_state->window_sort_stale = TRUE; + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +gboolean +shell_app_get_busy (ShellApp *app) +{ + if (app->running_state != NULL && + app->running_state->application_proxy != NULL && + shell_org_gtk_application_get_busy (app->running_state->application_proxy)) + return TRUE; + + return FALSE; +} + +static void +busy_changed_cb (GObject *object, + GParamSpec *pspec, + gpointer user_data) +{ + ShellApp *app = user_data; + + g_assert (SHELL_IS_APP (app)); + + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_BUSY]); +} + +static void +get_application_proxy (GObject *source, + GAsyncResult *result, + gpointer user_data) +{ + ShellApp *app = user_data; + ShellOrgGtkApplication *proxy; + g_autoptr (GError) error = NULL; + + g_assert (SHELL_IS_APP (app)); + + proxy = shell_org_gtk_application_proxy_new_finish (result, &error); + if (proxy != NULL) + { + app->running_state->application_proxy = proxy; + g_signal_connect (proxy, + "notify::busy", + G_CALLBACK (busy_changed_cb), + app); + if (shell_org_gtk_application_get_busy (proxy)) + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_BUSY]); + } + + if (app->running_state != NULL && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_clear_object (&app->running_state->cancellable); + + g_object_unref (app); +} + +static void +shell_app_ensure_busy_watch (ShellApp *app) +{ + ShellAppRunningState *running_state = app->running_state; + MetaWindow *window; + const gchar *object_path; + + if (running_state->application_proxy != NULL || + running_state->cancellable != NULL) + return; + + if (running_state->unique_bus_name == NULL) + return; + + window = g_slist_nth_data (running_state->windows, 0); + object_path = meta_window_get_gtk_application_object_path (window); + + if (object_path == NULL) + return; + + running_state->cancellable = g_cancellable_new(); + /* Take a reference to app to make sure it isn't finalized before + get_application_proxy runs */ + shell_org_gtk_application_proxy_new (running_state->session, + G_DBUS_PROXY_FLAGS_DO_NOT_AUTO_START, + running_state->unique_bus_name, + object_path, + running_state->cancellable, + get_application_proxy, + g_object_ref (app)); +} + +void +_shell_app_add_window (ShellApp *app, + MetaWindow *window) +{ + if (app->running_state && g_slist_find (app->running_state->windows, window)) + return; + + g_object_freeze_notify (G_OBJECT (app)); + + if (!app->running_state) + create_running_state (app); + + app->running_state->window_sort_stale = TRUE; + app->running_state->windows = g_slist_prepend (app->running_state->windows, g_object_ref (window)); + g_signal_connect_object (window, "notify::user-time", G_CALLBACK(shell_app_on_user_time_changed), app, 0); + g_signal_connect_object (window, "notify::skip-taskbar", G_CALLBACK(shell_app_on_skip_taskbar_changed), app, 0); + + shell_app_update_app_actions (app, window); + shell_app_ensure_busy_watch (app); + + if (!meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows++; + shell_app_sync_running_state (app); + + if (app->started_on_workspace >= 0 && !meta_window_is_on_all_workspaces (window)) + meta_window_change_workspace_by_index (window, app->started_on_workspace, FALSE); + app->started_on_workspace = -1; + + g_object_thaw_notify (G_OBJECT (app)); + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +void +_shell_app_remove_window (ShellApp *app, + MetaWindow *window) +{ + g_assert (app->running_state != NULL); + + if (!g_slist_find (app->running_state->windows, window)) + return; + + app->running_state->windows = g_slist_remove (app->running_state->windows, window); + + if (!meta_window_is_skip_taskbar (window)) + app->running_state->interesting_windows--; + shell_app_sync_running_state (app); + + if (app->running_state->windows == NULL) + g_clear_pointer (&app->running_state, unref_running_state); + + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_user_time_changed), app); + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(shell_app_on_skip_taskbar_changed), app); + if (window == app->fallback_icon_window) + { + g_signal_handlers_disconnect_by_func (window, G_CALLBACK(on_window_icon_changed), app); + app->fallback_icon_window = NULL; + + /* Select a new icon from a different window. */ + g_clear_object (&app->fallback_icon); + g_object_notify_by_pspec (G_OBJECT (app), props[PROP_ICON]); + } + + g_object_unref (window); + + g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0); +} + +/** + * shell_app_get_pids: + * @app: a #ShellApp + * + * Returns: (transfer container) (element-type int): An unordered list of process identifiers associated with this application. + */ +GSList * +shell_app_get_pids (ShellApp *app) +{ + GSList *result; + g_autoptr (GSList) windows = NULL; + GSList *iter; + + result = NULL; + windows = shell_app_get_windows (app); + for (iter = windows; iter; iter = iter->next) + { + MetaWindow *window = iter->data; + pid_t pid = meta_window_get_pid (window); + + if (pid < 1) + continue; + + /* Note in the (by far) common case, app will only have one pid, so + * we'll hit the first element, so don't worry about O(N^2) here. + */ + if (!g_slist_find (result, GINT_TO_POINTER (pid))) + result = g_slist_prepend (result, GINT_TO_POINTER (pid)); + } + return result; +} + +void +_shell_app_handle_startup_sequence (ShellApp *app, + MetaStartupSequence *sequence) +{ + gboolean starting = !meta_startup_sequence_get_completed (sequence); + + /* The Shell design calls for on application launch, the app title + * appears at top, and no X window is focused. So when we get + * a startup-notification for this app, transition it to STARTING + * if it's currently stopped, set it as our application focus, + * but focus the no_focus window. + */ + if (starting && shell_app_get_state (app) == SHELL_APP_STATE_STOPPED) + { + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + shell_app_state_transition (app, SHELL_APP_STATE_STARTING); + meta_display_unset_input_focus (display, + meta_startup_sequence_get_timestamp (sequence)); + } + + if (starting) + app->started_on_workspace = meta_startup_sequence_get_workspace (sequence); + else if (app->running_state && app->running_state->windows) + shell_app_state_transition (app, SHELL_APP_STATE_RUNNING); + else /* application have > 1 .desktop file */ + shell_app_state_transition (app, SHELL_APP_STATE_STOPPED); +} + +/** + * shell_app_request_quit: + * @app: A #ShellApp + * + * Initiate an asynchronous request to quit this application. + * The application may interact with the user, and the user + * might cancel the quit request from the application UI. + * + * This operation may not be supported for all applications. + * + * Returns: %TRUE if a quit request is supported for this application + */ +gboolean +shell_app_request_quit (ShellApp *app) +{ + GActionGroup *group = NULL; + GSList *iter; + + if (shell_app_get_state (app) != SHELL_APP_STATE_RUNNING) + return FALSE; + + /* First, check whether the app exports an explicit "quit" action + * that we can activate on the bus + */ + group = G_ACTION_GROUP (app->running_state->muxer); + + if (g_action_group_has_action (group, "app.quit") && + g_action_group_get_action_parameter_type (group, "app.quit") == NULL) + { + g_action_group_activate_action (group, "app.quit", NULL); + + return TRUE; + } + + /* Otherwise, fall back to closing all the app's windows */ + for (iter = app->running_state->windows; iter; iter = iter->next) + { + MetaWindow *win = iter->data; + + if (!meta_window_can_close (win)) + continue; + + meta_window_delete (win, shell_global_get_current_time (shell_global_get ())); + } + return TRUE; +} + +static void +child_context_setup (gpointer user_data) +{ + ShellGlobal *shell_global = user_data; + MetaContext *meta_context; + + g_object_get (shell_global, "context", &meta_context, NULL); + meta_context_restore_rlimit_nofile (meta_context, NULL); +} + +#if !defined(HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS) && defined(HAVE_SYSTEMD) +/* This sets up the launched application to log to the journal + * using its own identifier, instead of just "gnome-session". + */ +static void +app_child_setup (gpointer user_data) +{ + const char *appid = user_data; + int res; + int journalfd = sd_journal_stream_fd (appid, LOG_INFO, FALSE); + ShellGlobal *shell_global = shell_global_get (); + + if (journalfd >= 0) + { + do + res = dup2 (journalfd, 1); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + do + res = dup2 (journalfd, 2); + while (G_UNLIKELY (res == -1 && errno == EINTR)); + (void) close (journalfd); + } + + child_context_setup (shell_global); +} +#endif + +static void +wait_pid (GDesktopAppInfo *appinfo, + GPid pid, + gpointer user_data) +{ + g_child_watch_add (pid, (GChildWatchFunc) g_spawn_close_pid, NULL); +} + +static void +apply_discrete_gpu_env (GAppLaunchContext *context, + ShellGlobal *global) +{ + GDBusProxy *proxy; + GVariant* variant; + guint num_children, i; + + proxy = shell_global_get_switcheroo_control (global); + if (!proxy) + { + g_warning ("Could not apply discrete GPU environment, switcheroo-control not available"); + return; + } + + variant = shell_net_hadess_switcheroo_control_get_gpus (SHELL_NET_HADESS_SWITCHEROO_CONTROL (proxy)); + if (!variant) + { + g_warning ("Could not apply discrete GPU environment, no GPUs in list"); + return; + } + + num_children = g_variant_n_children (variant); + for (i = 0; i < num_children; i++) + { + g_autoptr(GVariant) gpu = NULL; + g_autoptr(GVariant) env = NULL; + g_autoptr(GVariant) default_variant = NULL; + g_autofree const char **env_s = NULL; + guint j; + + gpu = g_variant_get_child_value (variant, i); + if (!gpu || + !g_variant_is_of_type (gpu, G_VARIANT_TYPE ("a{s*}"))) + continue; + + /* Skip over the default GPU */ + default_variant = g_variant_lookup_value (gpu, "Default", NULL); + if (!default_variant || g_variant_get_boolean (default_variant)) + continue; + + env = g_variant_lookup_value (gpu, "Environment", NULL); + if (!env) + continue; + + env_s = g_variant_get_strv (env, NULL); + for (j = 0; env_s[j] != NULL; j = j + 2) + g_app_launch_context_setenv (context, env_s[j], env_s[j+1]); + return; + } + + g_debug ("Could not find discrete GPU in switcheroo-control, not applying environment"); +} + +/** + * shell_app_launch: + * @timestamp: Event timestamp, or 0 for current event timestamp + * @workspace: Start on this workspace, or -1 for default + * @gpu_pref: the GPU to prefer launching on + * @error: A #GError + */ +gboolean +shell_app_launch (ShellApp *app, + guint timestamp, + int workspace, + ShellAppLaunchGpu gpu_pref, + GError **error) +{ + ShellGlobal *global; + GAppLaunchContext *context; + gboolean ret; + GSpawnFlags flags; + gboolean discrete_gpu = FALSE; + ShellGlobal *shell_global = shell_global_get (); + + if (app->info == NULL) + { + MetaWindow *window = window_backed_app_get_window (app); + /* We don't use an error return if there no longer any windows, because the + * user attempting to activate a stale window backed app isn't something + * we would expect the caller to meaningfully handle or display an error + * message to the user. + */ + if (window) + meta_window_activate (window, timestamp); + return TRUE; + } + + global = shell_global_get (); + context = shell_global_create_app_launch_context (global, timestamp, workspace); + if (gpu_pref == SHELL_APP_LAUNCH_GPU_APP_PREF) + discrete_gpu = g_desktop_app_info_get_boolean (app->info, "PrefersNonDefaultGPU"); + else + discrete_gpu = (gpu_pref == SHELL_APP_LAUNCH_GPU_DISCRETE); + + if (discrete_gpu) + apply_discrete_gpu_env (context, global); + + /* Set LEAVE_DESCRIPTORS_OPEN in order to use an optimized gspawn + * codepath. The shell's open file descriptors should be marked CLOEXEC + * so that they are automatically closed even with this flag set. + */ + flags = G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD | + G_SPAWN_LEAVE_DESCRIPTORS_OPEN; + +#ifdef HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS + /* Optimized spawn path, avoiding a child_setup function */ + { + int journalfd = -1; + +#ifdef HAVE_SYSTEMD + journalfd = sd_journal_stream_fd (shell_app_get_id (app), LOG_INFO, FALSE); +#endif /* HAVE_SYSTEMD */ + + ret = g_desktop_app_info_launch_uris_as_manager_with_fds (app->info, NULL, + context, + flags, + child_context_setup, shell_global, + wait_pid, NULL, + -1, + journalfd, + journalfd, + error); + + if (journalfd >= 0) + (void) close (journalfd); + } +#else /* !HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */ + ret = g_desktop_app_info_launch_uris_as_manager (app->info, NULL, + context, + flags, +#ifdef HAVE_SYSTEMD + app_child_setup, (gpointer)shell_app_get_id (app), +#else + child_context_setup, shell_global, +#endif + wait_pid, NULL, + error); +#endif /* HAVE_GIO_DESKTOP_LAUNCH_URIS_WITH_FDS */ + g_object_unref (context); + + return ret; +} + +/** + * shell_app_launch_action: + * @app: the #ShellApp + * @action_name: the name of the action to launch (as obtained by + * g_desktop_app_info_list_actions()) + * @timestamp: Event timestamp, or 0 for current event timestamp + * @workspace: Start on this workspace, or -1 for default + */ +void +shell_app_launch_action (ShellApp *app, + const char *action_name, + guint timestamp, + int workspace) +{ + ShellGlobal *global; + GAppLaunchContext *context; + + global = shell_global_get (); + context = shell_global_create_app_launch_context (global, timestamp, workspace); + + g_desktop_app_info_launch_action (G_DESKTOP_APP_INFO (app->info), + action_name, context); + + g_object_unref (context); +} + +/** + * shell_app_get_app_info: + * @app: a #ShellApp + * + * Returns: (transfer none): The #GDesktopAppInfo for this app, or %NULL if backed by a window + */ +GDesktopAppInfo * +shell_app_get_app_info (ShellApp *app) +{ + return app->info; +} + +static void +create_running_state (ShellApp *app) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + g_assert (app->running_state == NULL); + + app->running_state = g_new0 (ShellAppRunningState, 1); + app->running_state->refcount = 1; + app->running_state->workspace_switch_id = + g_signal_connect (workspace_manager, "workspace-switched", + G_CALLBACK (shell_app_on_ws_switch), app); + + app->running_state->session = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL); + g_assert (app->running_state->session != NULL); + app->running_state->muxer = gtk_action_muxer_new (); +} + +void +shell_app_update_app_actions (ShellApp *app, + MetaWindow *window) +{ + const gchar *unique_bus_name; + + /* We assume that 'gtk-application-object-path' and + * 'gtk-app-menu-object-path' are the same for all windows which + * have it set. + * + * It could be possible, however, that the first window we see + * belonging to the app didn't have them set. For this reason, we + * take the values from the first window that has them set and ignore + * all the rest (until the app is stopped and restarted). + */ + + unique_bus_name = meta_window_get_gtk_unique_bus_name (window); + + if (g_strcmp0 (app->running_state->unique_bus_name, unique_bus_name) != 0) + { + const gchar *application_object_path; + GDBusActionGroup *actions; + + application_object_path = meta_window_get_gtk_application_object_path (window); + + if (application_object_path == NULL || unique_bus_name == NULL) + return; + + g_clear_pointer (&app->running_state->unique_bus_name, g_free); + app->running_state->unique_bus_name = g_strdup (unique_bus_name); + actions = g_dbus_action_group_get (app->running_state->session, unique_bus_name, application_object_path); + gtk_action_muxer_insert (app->running_state->muxer, "app", G_ACTION_GROUP (actions)); + g_object_unref (actions); + } +} + +static void +unref_running_state (ShellAppRunningState *state) +{ + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + MetaWorkspaceManager *workspace_manager = + meta_display_get_workspace_manager (display); + + g_assert (state->refcount > 0); + + state->refcount--; + if (state->refcount > 0) + return; + + g_clear_signal_handler (&state->workspace_switch_id, workspace_manager); + + g_clear_object (&state->application_proxy); + + if (state->cancellable != NULL) + { + g_cancellable_cancel (state->cancellable); + g_clear_object (&state->cancellable); + } + + g_clear_object (&state->muxer); + g_clear_object (&state->session); + g_clear_pointer (&state->unique_bus_name, g_free); + + g_free (state); +} + +/** + * shell_app_compare_by_name: + * @app: One app + * @other: The other app + * + * Order two applications by name. + * + * Returns: -1, 0, or 1; suitable for use as a comparison function + * for e.g. g_slist_sort() + */ +int +shell_app_compare_by_name (ShellApp *app, ShellApp *other) +{ + return strcmp (app->name_collation_key, other->name_collation_key); +} + +static void +shell_app_init (ShellApp *self) +{ + self->state = SHELL_APP_STATE_STOPPED; + self->started_on_workspace = -1; +} + +static void +shell_app_dispose (GObject *object) +{ + ShellApp *app = SHELL_APP (object); + + g_clear_object (&app->info); + g_clear_object (&app->fallback_icon); + + while (app->running_state) + _shell_app_remove_window (app, app->running_state->windows->data); + + /* We should have been transitioned when we removed all of our windows */ + g_assert (app->state == SHELL_APP_STATE_STOPPED); + g_assert (app->running_state == NULL); + + G_OBJECT_CLASS(shell_app_parent_class)->dispose (object); +} + +static void +shell_app_finalize (GObject *object) +{ + ShellApp *app = SHELL_APP (object); + + g_free (app->window_id_string); + + g_free (app->name_collation_key); + + G_OBJECT_CLASS(shell_app_parent_class)->finalize (object); +} + +static void +shell_app_class_init(ShellAppClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_app_get_property; + gobject_class->set_property = shell_app_set_property; + gobject_class->dispose = shell_app_dispose; + gobject_class->finalize = shell_app_finalize; + + shell_app_signals[WINDOWS_CHANGED] = g_signal_new ("windows-changed", + SHELL_TYPE_APP, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + /** + * ShellApp:state: + * + * The high-level state of the application, effectively whether it's + * running or not, or transitioning between those states. + */ + props[PROP_STATE] = + g_param_spec_enum ("state", + "State", + "Application state", + SHELL_TYPE_APP_STATE, + SHELL_APP_STATE_STOPPED, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:busy: + * + * Whether the application has marked itself as busy. + */ + props[PROP_BUSY] = + g_param_spec_boolean ("busy", + "Busy", + "Busy state", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:id: + * + * The id of this application (a desktop filename, or a special string + * like window:0xabcd1234) + */ + props[PROP_ID] = + g_param_spec_string ("id", + "Application id", + "The desktop file id of this ShellApp", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:icon: + * + * The #GIcon representing this ShellApp + */ + props[PROP_ICON] = + g_param_spec_object ("icon", + "GIcon", + "The GIcon representing this app", + G_TYPE_ICON, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:action-group: + * + * The #GDBusActionGroup associated with this ShellApp, if any. See the + * documentation of #GApplication and #GActionGroup for details. + */ + props[PROP_ACTION_GROUP] = + g_param_spec_object ("action-group", + "Application Action Group", + "The action group exported by the remote application", + G_TYPE_ACTION_GROUP, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellApp:app-info: + * + * The #GDesktopAppInfo associated with this ShellApp, if any. + */ + props[PROP_APP_INFO] = + g_param_spec_object ("app-info", + "DesktopAppInfo", + "The DesktopAppInfo associated with this app", + G_TYPE_DESKTOP_APP_INFO, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} diff --git a/src/shell-app.h b/src/shell-app.h new file mode 100644 index 0000000..bf6f45e --- /dev/null +++ b/src/shell-app.h @@ -0,0 +1,83 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_APP_H__ +#define __SHELL_APP_H__ + +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_APP (shell_app_get_type ()) +G_DECLARE_FINAL_TYPE (ShellApp, shell_app, SHELL, APP, GObject) + +typedef enum { + SHELL_APP_STATE_STOPPED, + SHELL_APP_STATE_STARTING, + SHELL_APP_STATE_RUNNING +} ShellAppState; + +typedef enum { + SHELL_APP_LAUNCH_GPU_APP_PREF = 0, + SHELL_APP_LAUNCH_GPU_DISCRETE, + SHELL_APP_LAUNCH_GPU_DEFAULT +} ShellAppLaunchGpu; + +const char *shell_app_get_id (ShellApp *app); + +GDesktopAppInfo *shell_app_get_app_info (ShellApp *app); + +ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size); +GIcon *shell_app_get_icon (ShellApp *app); +const char *shell_app_get_name (ShellApp *app); +const char *shell_app_get_description (ShellApp *app); +gboolean shell_app_is_window_backed (ShellApp *app); + +void shell_app_activate_window (ShellApp *app, MetaWindow *window, guint32 timestamp); + +void shell_app_activate (ShellApp *app); + +void shell_app_activate_full (ShellApp *app, + int workspace, + guint32 timestamp); + +void shell_app_open_new_window (ShellApp *app, + int workspace); +gboolean shell_app_can_open_new_window (ShellApp *app); + +ShellAppState shell_app_get_state (ShellApp *app); + +gboolean shell_app_request_quit (ShellApp *app); + +guint shell_app_get_n_windows (ShellApp *app); + +GSList *shell_app_get_windows (ShellApp *app); + +GSList *shell_app_get_pids (ShellApp *app); + +gboolean shell_app_is_on_workspace (ShellApp *app, MetaWorkspace *workspace); + +gboolean shell_app_launch (ShellApp *app, + guint timestamp, + int workspace, + ShellAppLaunchGpu gpu_pref, + GError **error); + +void shell_app_launch_action (ShellApp *app, + const char *action_name, + guint timestamp, + int workspace); + +int shell_app_compare_by_name (ShellApp *app, ShellApp *other); + +int shell_app_compare (ShellApp *app, ShellApp *other); + +void shell_app_update_window_actions (ShellApp *app, MetaWindow *window); +void shell_app_update_app_actions (ShellApp *app, MetaWindow *window); + +gboolean shell_app_get_busy (ShellApp *app); + +G_END_DECLS + +#endif /* __SHELL_APP_H__ */ diff --git a/src/shell-blur-effect.c b/src/shell-blur-effect.c new file mode 100644 index 0000000..0d4bb45 --- /dev/null +++ b/src/shell-blur-effect.c @@ -0,0 +1,907 @@ +/* shell-blur-effect.c + * + * Copyright 2019 Georges Basile Stavracas Neto + * + * 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#include "shell-blur-effect.h" + +#include "shell-enum-types.h" + +/** + * SECTION:shell-blur-effect + * @short_description: Blur effect for actors + * + * #ShellBlurEffect is a blur implementation based on Clutter. It also has + * an optional brightness property. + * + * # Modes + * + * #ShellBlurEffect can work in @SHELL_BLUR_MODE_BACKGROUND and @SHELL_BLUR_MODE_ACTOR + * modes. The actor mode blurs the actor itself, and all of its children. The + * background mode blurs the pixels beneath the actor, but not the actor itself. + * + * @SHELL_BLUR_MODE_BACKGROUND can be computationally expensive, since the contents + * beneath the actor cannot be cached, so beware of the performance implications + * of using this blur mode. + */ + +static const gchar *brightness_glsl_declarations = +"uniform float brightness; \n"; + +static const gchar *brightness_glsl = +" cogl_color_out.rgb *= brightness; \n"; + +#define MIN_DOWNSCALE_SIZE 256.f +#define MAX_SIGMA 6.f + +typedef enum +{ + ACTOR_PAINTED = 1 << 0, + BLUR_APPLIED = 1 << 1, +} CacheFlags; + +typedef struct +{ + CoglFramebuffer *framebuffer; + CoglPipeline *pipeline; + CoglTexture *texture; +} FramebufferData; + +struct _ShellBlurEffect +{ + ClutterEffect parent_instance; + + ClutterActor *actor; + + unsigned int tex_width; + unsigned int tex_height; + + /* The cached contents */ + FramebufferData actor_fb; + CacheFlags cache_flags; + + FramebufferData background_fb; + FramebufferData brightness_fb; + int brightness_uniform; + + ShellBlurMode mode; + float downscale_factor; + float brightness; + int sigma; +}; + +G_DEFINE_TYPE (ShellBlurEffect, shell_blur_effect, CLUTTER_TYPE_EFFECT) + +enum { + PROP_0, + PROP_SIGMA, + PROP_BRIGHTNESS, + PROP_MODE, + N_PROPS +}; + +static GParamSpec *properties [N_PROPS] = { NULL, }; + +static CoglPipeline* +create_base_pipeline (void) +{ + static CoglPipeline *base_pipeline = NULL; + + if (G_UNLIKELY (base_pipeline == NULL)) + { + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + base_pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_set_layer_null_texture (base_pipeline, 0); + cogl_pipeline_set_layer_filters (base_pipeline, + 0, + COGL_PIPELINE_FILTER_LINEAR, + COGL_PIPELINE_FILTER_LINEAR); + cogl_pipeline_set_layer_wrap_mode (base_pipeline, + 0, + COGL_PIPELINE_WRAP_MODE_CLAMP_TO_EDGE); + } + + return cogl_pipeline_copy (base_pipeline); +} + +static CoglPipeline* +create_brightness_pipeline (void) +{ + static CoglPipeline *brightness_pipeline = NULL; + + if (G_UNLIKELY (brightness_pipeline == NULL)) + { + CoglSnippet *snippet; + + brightness_pipeline = create_base_pipeline (); + + snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_FRAGMENT, + brightness_glsl_declarations, + brightness_glsl); + cogl_pipeline_add_snippet (brightness_pipeline, snippet); + cogl_object_unref (snippet); + } + + return cogl_pipeline_copy (brightness_pipeline); +} + + +static void +update_brightness (ShellBlurEffect *self, + uint8_t paint_opacity) +{ + cogl_pipeline_set_color4ub (self->brightness_fb.pipeline, + paint_opacity, + paint_opacity, + paint_opacity, + paint_opacity); + + if (self->brightness_uniform > -1) + { + cogl_pipeline_set_uniform_1f (self->brightness_fb.pipeline, + self->brightness_uniform, + self->brightness); + } +} + +static void +setup_projection_matrix (CoglFramebuffer *framebuffer, + float width, + float height) +{ + graphene_matrix_t projection; + + graphene_matrix_init_translate (&projection, + &GRAPHENE_POINT3D_INIT (-width / 2.0, + -height / 2.0, + 0.f)); + graphene_matrix_scale (&projection, 2.0 / width, -2.0 / height, 1.f); + + cogl_framebuffer_set_projection_matrix (framebuffer, &projection); +} + +static gboolean +update_fbo (FramebufferData *data, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + g_clear_pointer (&data->texture, cogl_object_unref); + g_clear_object (&data->framebuffer); + + float new_width = floorf (width / downscale_factor); + float new_height = floorf (height / downscale_factor); + + data->texture = cogl_texture_2d_new_with_size (ctx, new_width, new_height); + if (!data->texture) + return FALSE; + + cogl_pipeline_set_layer_texture (data->pipeline, 0, data->texture); + + data->framebuffer = + COGL_FRAMEBUFFER (cogl_offscreen_new_with_texture (data->texture)); + if (!data->framebuffer) + { + g_warning ("%s: Unable to create an Offscreen buffer", G_STRLOC); + return FALSE; + } + + setup_projection_matrix (data->framebuffer, new_width, new_height); + + return TRUE; +} + +static gboolean +update_actor_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + if (self->tex_width == width && + self->tex_height == height && + self->downscale_factor == downscale_factor && + self->actor_fb.framebuffer) + { + return TRUE; + } + + self->cache_flags &= ~ACTOR_PAINTED; + + return update_fbo (&self->actor_fb, width, height, downscale_factor); +} + +static gboolean +update_brightness_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height, + float downscale_factor) +{ + if (self->tex_width == width && + self->tex_height == height && + self->downscale_factor == downscale_factor && + self->brightness_fb.framebuffer) + { + return TRUE; + } + + return update_fbo (&self->brightness_fb, + width, height, + downscale_factor); +} + +static gboolean +update_background_fbo (ShellBlurEffect *self, + unsigned int width, + unsigned int height) +{ + if (self->tex_width == width && + self->tex_height == height && + self->background_fb.framebuffer) + { + return TRUE; + } + + return update_fbo (&self->background_fb, width, height, 1.0); +} + +static void +clear_framebuffer_data (FramebufferData *fb_data) +{ + g_clear_pointer (&fb_data->texture, cogl_object_unref); + g_clear_object (&fb_data->framebuffer); +} + +static float +calculate_downscale_factor (float width, + float height, + float sigma) +{ + float downscale_factor = 1.0; + float scaled_width = width; + float scaled_height = height; + float scaled_sigma = sigma; + + /* This is the algorithm used by Firefox; keep downscaling until either the + * blur radius is lower than the threshold, or the downscaled texture is too + * small. + */ + while (scaled_sigma > MAX_SIGMA && + scaled_width > MIN_DOWNSCALE_SIZE && + scaled_height > MIN_DOWNSCALE_SIZE) + { + downscale_factor *= 2.f; + + scaled_width = width / downscale_factor; + scaled_height = height / downscale_factor; + scaled_sigma = sigma / downscale_factor; + } + + return downscale_factor; +} + +static void +shell_blur_effect_set_actor (ClutterActorMeta *meta, + ClutterActor *actor) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (meta); + ClutterActorMetaClass *meta_class; + + meta_class = CLUTTER_ACTOR_META_CLASS (shell_blur_effect_parent_class); + meta_class->set_actor (meta, actor); + + /* clear out the previous state */ + clear_framebuffer_data (&self->actor_fb); + clear_framebuffer_data (&self->background_fb); + clear_framebuffer_data (&self->brightness_fb); + + /* we keep a back pointer here, to avoid going through the ActorMeta */ + self->actor = clutter_actor_meta_get_actor (meta); +} + +static void +update_actor_box (ShellBlurEffect *self, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + ClutterStageView *stage_view; + float box_scale_factor = 1.0f; + float origin_x, origin_y; + float width, height; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + clutter_actor_get_allocation_box (self->actor, source_actor_box); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + stage_view = clutter_paint_context_get_stage_view (paint_context); + + clutter_actor_get_transformed_position (self->actor, &origin_x, &origin_y); + clutter_actor_get_transformed_size (self->actor, &width, &height); + + if (stage_view) + { + cairo_rectangle_int_t stage_view_layout; + + box_scale_factor = clutter_stage_view_get_scale (stage_view); + clutter_stage_view_get_layout (stage_view, &stage_view_layout); + + origin_x -= stage_view_layout.x; + origin_y -= stage_view_layout.y; + } + else + { + /* If we're drawing off stage, just assume scale = 1, this won't work + * with stage-view scaling though. + */ + } + + clutter_actor_box_set_origin (source_actor_box, origin_x, origin_y); + clutter_actor_box_set_size (source_actor_box, width, height); + + clutter_actor_box_scale (source_actor_box, box_scale_factor); + break; + } + + clutter_actor_box_clamp_to_pixel (source_actor_box); +} + +static void +add_blurred_pipeline (ShellBlurEffect *self, + ClutterPaintNode *node, + uint8_t paint_opacity) +{ + g_autoptr (ClutterPaintNode) pipeline_node = NULL; + float width, height; + + /* Use the untransformed actor size here, since the framebuffer itself already + * has the actor transform matrix applied. + */ + clutter_actor_get_size (self->actor, &width, &height); + + update_brightness (self, paint_opacity); + + pipeline_node = clutter_pipeline_node_new (self->brightness_fb.pipeline); + clutter_paint_node_set_static_name (pipeline_node, "ShellBlurEffect (final)"); + clutter_paint_node_add_child (node, pipeline_node); + + clutter_paint_node_add_rectangle (pipeline_node, + &(ClutterActorBox) { + 0.f, 0.f, + width, + height, + }); +} + +static ClutterPaintNode * +create_blur_nodes (ShellBlurEffect *self, + ClutterPaintNode *node, + uint8_t paint_opacity) +{ + g_autoptr (ClutterPaintNode) brightness_node = NULL; + g_autoptr (ClutterPaintNode) blur_node = NULL; + float width; + float height; + + clutter_actor_get_size (self->actor, &width, &height); + + update_brightness (self, paint_opacity); + brightness_node = clutter_layer_node_new_to_framebuffer (self->brightness_fb.framebuffer, + self->brightness_fb.pipeline); + clutter_paint_node_set_static_name (brightness_node, "ShellBlurEffect (brightness)"); + clutter_paint_node_add_child (node, brightness_node); + clutter_paint_node_add_rectangle (brightness_node, + &(ClutterActorBox) { + 0.f, 0.f, + width, height, + }); + + blur_node = clutter_blur_node_new (self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + self->sigma / self->downscale_factor); + clutter_paint_node_set_static_name (blur_node, "ShellBlurEffect (blur)"); + clutter_paint_node_add_child (brightness_node, blur_node); + clutter_paint_node_add_rectangle (blur_node, + &(ClutterActorBox) { + 0.f, 0.f, + cogl_texture_get_width (self->brightness_fb.texture), + cogl_texture_get_height (self->brightness_fb.texture), + }); + + self->cache_flags |= BLUR_APPLIED; + + return g_steal_pointer (&blur_node); +} + +static void +paint_background (ShellBlurEffect *self, + ClutterPaintNode *node, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + g_autoptr (ClutterPaintNode) background_node = NULL; + g_autoptr (ClutterPaintNode) blit_node = NULL; + CoglFramebuffer *src; + float transformed_x; + float transformed_y; + float transformed_width; + float transformed_height; + + clutter_actor_box_get_origin (source_actor_box, + &transformed_x, + &transformed_y); + clutter_actor_box_get_size (source_actor_box, + &transformed_width, + &transformed_height); + + /* Background layer node */ + background_node = + clutter_layer_node_new_to_framebuffer (self->background_fb.framebuffer, + self->background_fb.pipeline); + clutter_paint_node_set_static_name (background_node, "ShellBlurEffect (background)"); + clutter_paint_node_add_child (node, background_node); + clutter_paint_node_add_rectangle (background_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + + /* Blit node */ + src = clutter_paint_context_get_framebuffer (paint_context); + blit_node = clutter_blit_node_new (src); + clutter_paint_node_set_static_name (blit_node, "ShellBlurEffect (blit)"); + clutter_paint_node_add_child (background_node, blit_node); + clutter_blit_node_add_blit_rectangle (CLUTTER_BLIT_NODE (blit_node), + transformed_x, + transformed_y, + 0, 0, + transformed_width, + transformed_height); +} + +static gboolean +update_framebuffers (ShellBlurEffect *self, + ClutterPaintContext *paint_context, + ClutterActorBox *source_actor_box) +{ + gboolean updated = FALSE; + float downscale_factor; + float height = -1; + float width = -1; + + clutter_actor_box_get_size (source_actor_box, &width, &height); + + downscale_factor = calculate_downscale_factor (width, height, self->sigma); + + updated = update_actor_fbo (self, width, height, downscale_factor) && + update_brightness_fbo (self, width, height, downscale_factor); + + if (self->mode == SHELL_BLUR_MODE_BACKGROUND) + updated = updated && update_background_fbo (self, width, height); + + self->tex_width = width; + self->tex_height = height; + self->downscale_factor = downscale_factor; + + return updated; +} + +static void +add_actor_node (ShellBlurEffect *self, + ClutterPaintNode *node, + int opacity) +{ + g_autoptr (ClutterPaintNode) actor_node = NULL; + + actor_node = clutter_actor_node_new (self->actor, opacity); + clutter_paint_node_add_child (node, actor_node); +} + +static void +paint_actor_offscreen (ShellBlurEffect *self, + ClutterPaintNode *node, + ClutterEffectPaintFlags flags) +{ + gboolean actor_dirty; + + actor_dirty = (flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY) != 0; + + /* The actor offscreen framebuffer is updated already */ + if (actor_dirty || !(self->cache_flags & ACTOR_PAINTED)) + { + g_autoptr (ClutterPaintNode) transform_node = NULL; + g_autoptr (ClutterPaintNode) layer_node = NULL; + graphene_matrix_t transform; + + /* Layer node */ + layer_node = clutter_layer_node_new_to_framebuffer (self->actor_fb.framebuffer, + self->actor_fb.pipeline); + clutter_paint_node_set_static_name (layer_node, "ShellBlurEffect (actor offscreen)"); + clutter_paint_node_add_child (node, layer_node); + clutter_paint_node_add_rectangle (layer_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + + /* Transform node */ + graphene_matrix_init_scale (&transform, + 1.f / self->downscale_factor, + 1.f / self->downscale_factor, + 1.f); + transform_node = clutter_transform_node_new (&transform); + clutter_paint_node_set_static_name (transform_node, "ShellBlurEffect (downscale)"); + clutter_paint_node_add_child (layer_node, transform_node); + + /* Actor node */ + add_actor_node (self, transform_node, 255); + + self->cache_flags |= ACTOR_PAINTED; + } + else + { + g_autoptr (ClutterPaintNode) pipeline_node = NULL; + + pipeline_node = clutter_pipeline_node_new (self->actor_fb.pipeline); + clutter_paint_node_set_static_name (pipeline_node, + "ShellBlurEffect (actor texture)"); + clutter_paint_node_add_child (node, pipeline_node); + clutter_paint_node_add_rectangle (pipeline_node, + &(ClutterActorBox) { + 0.f, 0.f, + self->tex_width / self->downscale_factor, + self->tex_height / self->downscale_factor, + }); + } +} + +static gboolean +needs_repaint (ShellBlurEffect *self, + ClutterEffectPaintFlags flags) +{ + gboolean actor_cached; + gboolean blur_cached; + gboolean actor_dirty; + + actor_dirty = (flags & CLUTTER_EFFECT_PAINT_ACTOR_DIRTY) != 0; + blur_cached = (self->cache_flags & BLUR_APPLIED) != 0; + actor_cached = (self->cache_flags & ACTOR_PAINTED) != 0; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + return actor_dirty || !blur_cached || !actor_cached; + + case SHELL_BLUR_MODE_BACKGROUND: + return TRUE; + } + + return TRUE; +} + +static void +shell_blur_effect_paint_node (ClutterEffect *effect, + ClutterPaintNode *node, + ClutterPaintContext *paint_context, + ClutterEffectPaintFlags flags) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (effect); + uint8_t paint_opacity; + + g_assert (self->actor != NULL); + + if (self->sigma > 0) + { + g_autoptr (ClutterPaintNode) blur_node = NULL; + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + paint_opacity = clutter_actor_get_paint_opacity (self->actor); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + paint_opacity = 255; + break; + + default: + g_assert_not_reached(); + break; + } + + if (needs_repaint (self, flags)) + { + ClutterActorBox source_actor_box; + + update_actor_box (self, paint_context, &source_actor_box); + + /* Failing to create or update the offscreen framebuffers prevents + * the entire effect to be applied. + */ + if (!update_framebuffers (self, paint_context, &source_actor_box)) + goto fail; + + blur_node = create_blur_nodes (self, node, paint_opacity); + + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + paint_actor_offscreen (self, blur_node, flags); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + paint_background (self, blur_node, paint_context, &source_actor_box); + break; + } + } + else + { + /* Use the cached pipeline if no repaint is needed */ + add_blurred_pipeline (self, node, paint_opacity); + } + + /* Background blur needs to paint the actor after painting the blurred + * background. + */ + switch (self->mode) + { + case SHELL_BLUR_MODE_ACTOR: + break; + + case SHELL_BLUR_MODE_BACKGROUND: + add_actor_node (self, node, -1); + break; + } + + return; + } + +fail: + /* When no blur is applied, or the offscreen framebuffers + * couldn't be created, fallback to simply painting the actor. + */ + add_actor_node (self, node, -1); +} + +static void +shell_blur_effect_finalize (GObject *object) +{ + ShellBlurEffect *self = (ShellBlurEffect *)object; + + clear_framebuffer_data (&self->actor_fb); + clear_framebuffer_data (&self->background_fb); + clear_framebuffer_data (&self->brightness_fb); + + g_clear_pointer (&self->actor_fb.pipeline, cogl_object_unref); + g_clear_pointer (&self->background_fb.pipeline, cogl_object_unref); + g_clear_pointer (&self->brightness_fb.pipeline, cogl_object_unref); + + G_OBJECT_CLASS (shell_blur_effect_parent_class)->finalize (object); +} + +static void +shell_blur_effect_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (object); + + switch (prop_id) + { + case PROP_SIGMA: + g_value_set_int (value, self->sigma); + break; + + case PROP_BRIGHTNESS: + g_value_set_float (value, self->brightness); + break; + + case PROP_MODE: + g_value_set_enum (value, self->mode); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shell_blur_effect_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellBlurEffect *self = SHELL_BLUR_EFFECT (object); + + switch (prop_id) + { + case PROP_SIGMA: + shell_blur_effect_set_sigma (self, g_value_get_int (value)); + break; + + case PROP_BRIGHTNESS: + shell_blur_effect_set_brightness (self, g_value_get_float (value)); + break; + + case PROP_MODE: + shell_blur_effect_set_mode (self, g_value_get_enum (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + +static void +shell_blur_effect_class_init (ShellBlurEffectClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorMetaClass *meta_class = CLUTTER_ACTOR_META_CLASS (klass); + ClutterEffectClass *effect_class = CLUTTER_EFFECT_CLASS (klass); + + object_class->finalize = shell_blur_effect_finalize; + object_class->get_property = shell_blur_effect_get_property; + object_class->set_property = shell_blur_effect_set_property; + + meta_class->set_actor = shell_blur_effect_set_actor; + + effect_class->paint_node = shell_blur_effect_paint_node; + + properties[PROP_SIGMA] = + g_param_spec_int ("sigma", + "Sigma", + "Sigma", + 0, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_BRIGHTNESS] = + g_param_spec_float ("brightness", + "Brightness", + "Brightness", + 0.f, 1.f, 1.f, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + properties[PROP_MODE] = + g_param_spec_enum ("mode", + "Blur mode", + "Blur mode", + SHELL_TYPE_BLUR_MODE, + SHELL_BLUR_MODE_ACTOR, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, N_PROPS, properties); +} + +static void +shell_blur_effect_init (ShellBlurEffect *self) +{ + self->mode = SHELL_BLUR_MODE_ACTOR; + self->sigma = 0; + self->brightness = 1.f; + + self->actor_fb.pipeline = create_base_pipeline (); + self->background_fb.pipeline = create_base_pipeline (); + self->brightness_fb.pipeline = create_brightness_pipeline (); + self->brightness_uniform = + cogl_pipeline_get_uniform_location (self->brightness_fb.pipeline, "brightness"); +} + +ShellBlurEffect * +shell_blur_effect_new (void) +{ + return g_object_new (SHELL_TYPE_BLUR_EFFECT, NULL); +} + +int +shell_blur_effect_get_sigma (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), -1); + + return self->sigma; +} + +void +shell_blur_effect_set_sigma (ShellBlurEffect *self, + int sigma) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->sigma == sigma) + return; + + self->sigma = sigma; + self->cache_flags &= ~BLUR_APPLIED; + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_SIGMA]); +} + +float +shell_blur_effect_get_brightness (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), FALSE); + + return self->brightness; +} + +void +shell_blur_effect_set_brightness (ShellBlurEffect *self, + float brightness) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->brightness == brightness) + return; + + self->brightness = brightness; + self->cache_flags &= ~BLUR_APPLIED; + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_BRIGHTNESS]); +} + +ShellBlurMode +shell_blur_effect_get_mode (ShellBlurEffect *self) +{ + g_return_val_if_fail (SHELL_IS_BLUR_EFFECT (self), -1); + + return self->mode; +} + +void +shell_blur_effect_set_mode (ShellBlurEffect *self, + ShellBlurMode mode) +{ + g_return_if_fail (SHELL_IS_BLUR_EFFECT (self)); + + if (self->mode == mode) + return; + + self->mode = mode; + self->cache_flags &= ~BLUR_APPLIED; + + switch (mode) + { + case SHELL_BLUR_MODE_ACTOR: + clear_framebuffer_data (&self->background_fb); + break; + + case SHELL_BLUR_MODE_BACKGROUND: + default: + /* Do nothing */ + break; + } + + if (self->actor) + clutter_effect_queue_repaint (CLUTTER_EFFECT (self)); + + g_object_notify_by_pspec (G_OBJECT (self), properties[PROP_MODE]); +} diff --git a/src/shell-blur-effect.h b/src/shell-blur-effect.h new file mode 100644 index 0000000..a7486cc --- /dev/null +++ b/src/shell-blur-effect.h @@ -0,0 +1,57 @@ +/* shell-blur-effect.h + * + * Copyright 2019 Georges Basile Stavracas Neto + * + * 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 . + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#pragma once + +#include + +G_BEGIN_DECLS + +/** + * ShellBlurMode: + * @SHELL_BLUR_MODE_ACTOR: blur the actor contents, and its children + * @SHELL_BLUR_MODE_BACKGROUND: blur what's beneath the actor + * + * The mode of blurring of the effect. + */ +typedef enum +{ + SHELL_BLUR_MODE_ACTOR, + SHELL_BLUR_MODE_BACKGROUND, +} ShellBlurMode; + +#define SHELL_TYPE_BLUR_EFFECT (shell_blur_effect_get_type()) +G_DECLARE_FINAL_TYPE (ShellBlurEffect, shell_blur_effect, SHELL, BLUR_EFFECT, ClutterEffect) + +ShellBlurEffect *shell_blur_effect_new (void); + +int shell_blur_effect_get_sigma (ShellBlurEffect *self); +void shell_blur_effect_set_sigma (ShellBlurEffect *self, + int sigma); + +float shell_blur_effect_get_brightness (ShellBlurEffect *self); +void shell_blur_effect_set_brightness (ShellBlurEffect *self, + float brightness); + +ShellBlurMode shell_blur_effect_get_mode (ShellBlurEffect *self); +void shell_blur_effect_set_mode (ShellBlurEffect *self, + ShellBlurMode mode); + +G_END_DECLS diff --git a/src/shell-embedded-window-private.h b/src/shell-embedded-window-private.h new file mode 100644 index 0000000..5714af9 --- /dev/null +++ b/src/shell-embedded-window-private.h @@ -0,0 +1,20 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ +#define __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ + +#include "shell-embedded-window.h" +#include "shell-gtk-embed.h" + +void _shell_embedded_window_set_actor (ShellEmbeddedWindow *window, + ShellGtkEmbed *embed); + +void _shell_embedded_window_allocate (ShellEmbeddedWindow *window, + int x, + int y, + int width, + int height); + +void _shell_embedded_window_map (ShellEmbeddedWindow *window); +void _shell_embedded_window_unmap (ShellEmbeddedWindow *window); + +#endif /* __SHELL_EMBEDDED_WINDOW_PRIVATE_H__ */ diff --git a/src/shell-embedded-window.c b/src/shell-embedded-window.c new file mode 100644 index 0000000..8fd6112 --- /dev/null +++ b/src/shell-embedded-window.c @@ -0,0 +1,247 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include + +#include "shell-embedded-window-private.h" + +/* This type is a subclass of GtkWindow that ties the window to a + * ShellGtkEmbed; the resizing logic is bound to the clutter logic. + * + * The typical usage we might expect is + * + * - ShellEmbeddedWindow is created and filled with content + * - ShellEmbeddedWindow is shown with gtk_widget_show_all() + * - ShellGtkEmbed is created for the ShellEmbeddedWindow + * - actor is added to a stage + * + * The way it works is that the GtkWindow is mapped if and only if both: + * + * - gtk_widget_visible (window) [widget has been shown] + * - Actor is mapped [actor and all parents visible, actor in stage] + */ + +enum { + PROP_0 +}; + +typedef struct _ShellEmbeddedWindowPrivate ShellEmbeddedWindowPrivate; + +struct _ShellEmbeddedWindowPrivate { + ShellGtkEmbed *actor; + + GdkRectangle position; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellEmbeddedWindow, + shell_embedded_window, + GTK_TYPE_WINDOW); + +/* + * The normal gtk_window_show() starts all of the complicated asynchronous + * window resizing code running; we don't want or need any of that. + * Bypassing the normal code does mean that the extra geometry management + * available on GtkWindow: gridding, maximum sizes, etc, is ignored; we + * don't really want that anyways - we just want a way of embedding a + * GtkWidget into a Clutter stage. + */ +static void +shell_embedded_window_show (GtkWidget *widget) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (widget); + ShellEmbeddedWindowPrivate *priv; + GtkWidgetClass *widget_class; + + priv = shell_embedded_window_get_instance_private (window); + + /* Skip GtkWindow, but run the default GtkWidget handling which + * marks the widget visible */ + widget_class = g_type_class_peek (GTK_TYPE_WIDGET); + widget_class->show (widget); + + if (priv->actor) + { + /* Size is 0x0 if the GtkWindow is not shown */ + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); + + if (clutter_actor_is_realized (CLUTTER_ACTOR (priv->actor))) + gtk_widget_map (widget); + } +} + +static void +shell_embedded_window_hide (GtkWidget *widget) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (widget); + ShellEmbeddedWindowPrivate *priv; + + priv = shell_embedded_window_get_instance_private (window); + + if (priv->actor) + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); + + GTK_WIDGET_CLASS (shell_embedded_window_parent_class)->hide (widget); +} + +static gboolean +shell_embedded_window_configure_event (GtkWidget *widget, + GdkEventConfigure *event) +{ + /* Normally a configure event coming back from X triggers the + * resizing logic inside GtkWindow; we just ignore them + * since we are handling the resizing logic separately. + */ + return FALSE; +} + +static void +shell_embedded_window_check_resize (GtkContainer *container) +{ + ShellEmbeddedWindow *window = SHELL_EMBEDDED_WINDOW (container); + ShellEmbeddedWindowPrivate *priv; + + priv = shell_embedded_window_get_instance_private (window); + + /* Check resize is called when a resize is queued on something + * inside the GtkWindow; we need to make sure that in response + * to this gtk_widget_size_request() and then + * gtk_widget_size_allocate() are called; we defer to the Clutter + * logic and assume it will do the right thing. + */ + if (priv->actor) + clutter_actor_queue_relayout (CLUTTER_ACTOR (priv->actor)); +} + +static GObject * +shell_embedded_window_constructor (GType gtype, + guint n_properties, + GObjectConstructParam *properties) +{ + GObject *object; + GObjectClass *parent_class; + + parent_class = G_OBJECT_CLASS (shell_embedded_window_parent_class); + object = parent_class->constructor (gtype, n_properties, properties); + + /* Setting the resize mode to immediate means that calling queue_resize() + * on a widget within the window will immediately call check_resize() + * to be called, instead of having it queued to an idle. From our perspective, + * this is ideal since we just are going to queue a resize to Clutter's + * idle resize anyways. + */ + g_object_set (object, + "app-paintable", TRUE, + "resize-mode", GTK_RESIZE_IMMEDIATE, + "type", GTK_WINDOW_POPUP, + NULL); + + return object; +} + +static void +shell_embedded_window_class_init (ShellEmbeddedWindowClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); + GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass); + + object_class->constructor = shell_embedded_window_constructor; + + widget_class->show = shell_embedded_window_show; + widget_class->hide = shell_embedded_window_hide; + widget_class->configure_event = shell_embedded_window_configure_event; + + container_class->check_resize = shell_embedded_window_check_resize; +} + +static void +shell_embedded_window_init (ShellEmbeddedWindow *window) +{ +} + +/* + * Private routines called by ShellGtkEmbed + */ + +void +_shell_embedded_window_set_actor (ShellEmbeddedWindow *window, + ShellGtkEmbed *actor) + +{ + ShellEmbeddedWindowPrivate *priv; + + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + priv = shell_embedded_window_get_instance_private (window); + priv->actor = actor; + + if (actor && + clutter_actor_is_mapped (CLUTTER_ACTOR (actor)) && + gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_map (GTK_WIDGET (window)); +} + +void +_shell_embedded_window_allocate (ShellEmbeddedWindow *window, + int x, + int y, + int width, + int height) +{ + ShellEmbeddedWindowPrivate *priv; + GtkAllocation allocation; + + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + priv = shell_embedded_window_get_instance_private (window); + + if (priv->position.x == x && + priv->position.y == y && + priv->position.width == width && + priv->position.height == height) + return; + + priv->position.x = x; + priv->position.y = y; + priv->position.width = width; + priv->position.height = height; + + if (gtk_widget_get_realized (GTK_WIDGET (window))) + gdk_window_move_resize (gtk_widget_get_window (GTK_WIDGET (window)), + x, y, width, height); + + allocation.x = 0; + allocation.y = 0; + allocation.width = width; + allocation.height = height; + + gtk_widget_size_allocate (GTK_WIDGET (window), &allocation); +} + +void +_shell_embedded_window_map (ShellEmbeddedWindow *window) +{ + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + if (gtk_widget_get_visible (GTK_WIDGET (window))) + gtk_widget_map (GTK_WIDGET (window)); +} + +void +_shell_embedded_window_unmap (ShellEmbeddedWindow *window) +{ + g_return_if_fail (SHELL_IS_EMBEDDED_WINDOW (window)); + + gtk_widget_unmap (GTK_WIDGET (window)); +} + +/* + * Public API + */ +GtkWidget * +shell_embedded_window_new (void) +{ + return g_object_new (SHELL_TYPE_EMBEDDED_WINDOW, + NULL); +} diff --git a/src/shell-embedded-window.h b/src/shell-embedded-window.h new file mode 100644 index 0000000..835165b --- /dev/null +++ b/src/shell-embedded-window.h @@ -0,0 +1,19 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_EMBEDDED_WINDOW_H__ +#define __SHELL_EMBEDDED_WINDOW_H__ + +#include +#include + +#define SHELL_TYPE_EMBEDDED_WINDOW (shell_embedded_window_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellEmbeddedWindow, shell_embedded_window, + SHELL, EMBEDDED_WINDOW, GtkWindow) + +struct _ShellEmbeddedWindowClass +{ + GtkWindowClass parent_class; +}; + +GtkWidget *shell_embedded_window_new (void); + +#endif /* __SHELL_EMBEDDED_WINDOW_H__ */ diff --git a/src/shell-global-private.h b/src/shell-global-private.h new file mode 100644 index 0000000..9969691 --- /dev/null +++ b/src/shell-global-private.h @@ -0,0 +1,23 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLOBAL_PRIVATE_H__ +#define __SHELL_GLOBAL_PRIVATE_H__ + +#include "shell-global.h" + +#include + +void _shell_global_init (const char *first_property_name, + ...); +void _shell_global_set_plugin (ShellGlobal *global, + MetaPlugin *plugin); + +void _shell_global_destroy_gjs_context (ShellGlobal *global); + +GjsContext *_shell_global_get_gjs_context (ShellGlobal *global); + +gboolean _shell_global_check_xdnd_event (ShellGlobal *global, + XEvent *xev); + +void _shell_global_locate_pointer (ShellGlobal *global); + +#endif /* __SHELL_GLOBAL_PRIVATE_H__ */ diff --git a/src/shell-global.c b/src/shell-global.c new file mode 100644 index 0000000..0ccdb10 --- /dev/null +++ b/src/shell-global.c @@ -0,0 +1,1860 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef HAVE_SYS_RESOURCE_H +#include +#endif +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define GNOME_DESKTOP_USE_UNSTABLE_API +#include + +#if defined __OpenBSD__ || defined __FreeBSD__ +#include +#endif + +#include "shell-enum-types.h" +#include "shell-global-private.h" +#include "shell-perf-log.h" +#include "shell-window-tracker.h" +#include "shell-wm.h" +#include "shell-util.h" +#include "st.h" +#include "switcheroo-control.h" + +static ShellGlobal *the_object = NULL; + +struct _ShellGlobal { + GObject parent; + + ClutterStage *stage; + + MetaBackend *backend; + MetaContext *meta_context; + MetaDisplay *meta_display; + MetaWorkspaceManager *workspace_manager; + Display *xdisplay; + + char *session_mode; + + XserverRegion input_region; + + GjsContext *js_context; + MetaPlugin *plugin; + ShellWM *wm; + GSettings *settings; + const char *datadir; + char *imagedir; + char *userdatadir; + GFile *userdatadir_path; + GFile *runtime_state_path; + + StFocusManager *focus_manager; + + guint work_count; + GSList *leisure_closures; + guint leisure_function_id; + + GHashTable *save_ops; + + gboolean frame_timestamps; + gboolean frame_finish_timestamp; + + GDBusProxy *switcheroo_control; + GCancellable *switcheroo_cancellable; +}; + +enum { + PROP_0, + + PROP_SESSION_MODE, + PROP_BACKEND, + PROP_CONTEXT, + PROP_DISPLAY, + PROP_WORKSPACE_MANAGER, + PROP_SCREEN_WIDTH, + PROP_SCREEN_HEIGHT, + PROP_STAGE, + PROP_WINDOW_GROUP, + PROP_TOP_WINDOW_GROUP, + PROP_WINDOW_MANAGER, + PROP_SETTINGS, + PROP_DATADIR, + PROP_IMAGEDIR, + PROP_USERDATADIR, + PROP_FOCUS_MANAGER, + PROP_FRAME_TIMESTAMPS, + PROP_FRAME_FINISH_TIMESTAMP, + PROP_SWITCHEROO_CONTROL, + + N_PROPS +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +/* Signals */ +enum +{ + NOTIFY_ERROR, + LOCATE_POINTER, + LAST_SIGNAL +}; + +G_DEFINE_TYPE(ShellGlobal, shell_global, G_TYPE_OBJECT); + +static guint shell_global_signals [LAST_SIGNAL] = { 0 }; + +static void +got_switcheroo_control_gpus_property_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + ShellGlobal *global; + GError *error = NULL; + GVariant *gpus; + + gpus = g_dbus_connection_call_finish (G_DBUS_CONNECTION (source_object), + res, &error); + if (!gpus) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Could not get GPUs property from switcheroo-control: %s", error->message); + g_clear_error (&error); + return; + } + + global = user_data; + g_dbus_proxy_set_cached_property (global->switcheroo_control, "GPUs", gpus); + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); +} + +static void +switcheroo_control_ready_cb (GObject *source_object, + GAsyncResult *res, + gpointer user_data) +{ + ShellGlobal *global; + GError *error = NULL; + ShellNetHadessSwitcherooControl *control; + g_auto(GStrv) cached_props = NULL; + + control = shell_net_hadess_switcheroo_control_proxy_new_for_bus_finish (res, &error); + if (!control) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_debug ("Could not get switcheroo-control GDBusProxy: %s", error->message); + g_clear_error (&error); + return; + } + + global = user_data; + global->switcheroo_control = G_DBUS_PROXY (control); + g_debug ("Got switcheroo-control proxy successfully"); + + cached_props = g_dbus_proxy_get_cached_property_names (global->switcheroo_control); + if (cached_props != NULL && g_strv_contains ((const gchar * const *) cached_props, "GPUs")) + { + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); + return; + } + /* Delay property notification until we have all the properties gathered */ + + g_dbus_connection_call (g_dbus_proxy_get_connection (global->switcheroo_control), + g_dbus_proxy_get_name (global->switcheroo_control), + g_dbus_proxy_get_object_path (global->switcheroo_control), + "org.freedesktop.DBus.Properties", + "Get", + g_variant_new ("(ss)", + g_dbus_proxy_get_interface_name (global->switcheroo_control), + "GPUs"), + NULL, + G_DBUS_CALL_FLAGS_NONE, + -1, + global->switcheroo_cancellable, + got_switcheroo_control_gpus_property_cb, + user_data); +} + +static void +shell_global_set_property(GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + switch (prop_id) + { + case PROP_SESSION_MODE: + g_clear_pointer (&global->session_mode, g_free); + global->session_mode = g_ascii_strdown (g_value_get_string (value), -1); + break; + case PROP_FRAME_TIMESTAMPS: + { + gboolean enable = g_value_get_boolean (value); + + if (global->frame_timestamps != enable) + { + global->frame_timestamps = enable; + g_object_notify_by_pspec (object, props[PROP_FRAME_TIMESTAMPS]); + } + } + break; + case PROP_FRAME_FINISH_TIMESTAMP: + { + gboolean enable = g_value_get_boolean (value); + + if (global->frame_finish_timestamp != enable) + { + global->frame_finish_timestamp = enable; + g_object_notify_by_pspec (object, props[PROP_FRAME_FINISH_TIMESTAMP]); + } + } + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_global_get_property(GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + switch (prop_id) + { + case PROP_SESSION_MODE: + g_value_set_string (value, shell_global_get_session_mode (global)); + break; + case PROP_BACKEND: + g_value_set_object (value, global->backend); + break; + case PROP_CONTEXT: + g_value_set_object (value, global->meta_context); + break; + case PROP_DISPLAY: + g_value_set_object (value, global->meta_display); + break; + case PROP_WORKSPACE_MANAGER: + g_value_set_object (value, global->workspace_manager); + break; + case PROP_SCREEN_WIDTH: + { + int width, height; + + meta_display_get_size (global->meta_display, &width, &height); + g_value_set_int (value, width); + } + break; + case PROP_SCREEN_HEIGHT: + { + int width, height; + + meta_display_get_size (global->meta_display, &width, &height); + g_value_set_int (value, height); + } + break; + case PROP_STAGE: + g_value_set_object (value, global->stage); + break; + case PROP_WINDOW_GROUP: + g_value_set_object (value, meta_get_window_group_for_display (global->meta_display)); + break; + case PROP_TOP_WINDOW_GROUP: + g_value_set_object (value, meta_get_top_window_group_for_display (global->meta_display)); + break; + case PROP_WINDOW_MANAGER: + g_value_set_object (value, global->wm); + break; + case PROP_SETTINGS: + g_value_set_object (value, global->settings); + break; + case PROP_DATADIR: + g_value_set_string (value, global->datadir); + break; + case PROP_IMAGEDIR: + g_value_set_string (value, global->imagedir); + break; + case PROP_USERDATADIR: + g_value_set_string (value, global->userdatadir); + break; + case PROP_FOCUS_MANAGER: + g_value_set_object (value, global->focus_manager); + break; + case PROP_FRAME_TIMESTAMPS: + g_value_set_boolean (value, global->frame_timestamps); + break; + case PROP_FRAME_FINISH_TIMESTAMP: + g_value_set_boolean (value, global->frame_finish_timestamp); + break; + case PROP_SWITCHEROO_CONTROL: + g_value_set_object (value, global->switcheroo_control); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +switcheroo_appeared_cb (GDBusConnection *connection, + const char *name, + const char *name_owner, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + g_debug ("switcheroo-control appeared"); + shell_net_hadess_switcheroo_control_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, + G_DBUS_PROXY_FLAGS_NONE, + "net.hadess.SwitcherooControl", + "/net/hadess/SwitcherooControl", + global->switcheroo_cancellable, + switcheroo_control_ready_cb, + global); +} + +static void +switcheroo_vanished_cb (GDBusConnection *connection, + const char *name, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + g_debug ("switcheroo-control vanished"); + g_clear_object (&global->switcheroo_control); + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SWITCHEROO_CONTROL]); +} + +static void +shell_global_init (ShellGlobal *global) +{ + const char *datadir = g_getenv ("GNOME_SHELL_DATADIR"); + const char *shell_js = g_getenv("GNOME_SHELL_JS"); + char *imagedir, **search_path; + char *path; + const char *byteorder_string; + + if (!datadir) + datadir = GNOME_SHELL_DATADIR; + global->datadir = datadir; + + /* We make sure imagedir ends with a '/', since the JS won't have + * access to g_build_filename() and so will end up just + * concatenating global.imagedir to a filename. + */ + imagedir = g_build_filename (datadir, "images/", NULL); + if (g_file_test (imagedir, G_FILE_TEST_IS_DIR)) + global->imagedir = imagedir; + else + { + g_free (imagedir); + global->imagedir = g_strdup_printf ("%s/", datadir); + } + + /* Ensure config dir exists for later use */ + global->userdatadir = g_build_filename (g_get_user_data_dir (), "gnome-shell", NULL); + g_mkdir_with_parents (global->userdatadir, 0700); + global->userdatadir_path = g_file_new_for_path (global->userdatadir); + +#if G_BYTE_ORDER == G_LITTLE_ENDIAN + byteorder_string = "LE"; +#else + byteorder_string = "BE"; +#endif + + /* And the runtime state */ + path = g_strdup_printf ("%s/gnome-shell/runtime-state-%s.%s", + g_get_user_runtime_dir (), + byteorder_string, + XDisplayName (NULL)); + (void) g_mkdir_with_parents (path, 0700); + global->runtime_state_path = g_file_new_for_path (path); + g_free (path); + + global->settings = g_settings_new ("org.gnome.shell"); + + if (shell_js) + { + int i, j; + search_path = g_strsplit (shell_js, ":", -1); + + /* The naive g_strsplit above will split 'resource:///foo/bar' into 'resource', + * '///foo/bar'. Combine these back together by looking for a literal 'resource' + * in the array. */ + for (i = 0, j = 0; search_path[i];) + { + char *out; + + if (strcmp (search_path[i], "resource") == 0 && search_path[i + 1] != NULL) + { + out = g_strconcat (search_path[i], ":", search_path[i + 1], NULL); + g_free (search_path[i]); + g_free (search_path[i + 1]); + i += 2; + } + else + { + out = search_path[i]; + i += 1; + } + + search_path[j++] = out; + } + + search_path[j] = NULL; /* NULL-terminate the now possibly shorter array */ + } + else + { + search_path = g_malloc0 (2 * sizeof (char *)); + search_path[0] = g_strdup ("resource:///org/gnome/shell"); + } + + global->js_context = g_object_new (GJS_TYPE_CONTEXT, + "search-path", search_path, + NULL); + + g_strfreev (search_path); + + global->save_ops = g_hash_table_new_full (g_file_hash, + (GEqualFunc) g_file_equal, + g_object_unref, g_object_unref); + + global->switcheroo_cancellable = g_cancellable_new (); + g_bus_watch_name (G_BUS_TYPE_SYSTEM, + "net.hadess.SwitcherooControl", + G_BUS_NAME_WATCHER_FLAGS_NONE, + switcheroo_appeared_cb, + switcheroo_vanished_cb, + global, + NULL); +} + +static void +shell_global_finalize (GObject *object) +{ + ShellGlobal *global = SHELL_GLOBAL (object); + + g_clear_object (&global->js_context); + g_object_unref (global->settings); + + the_object = NULL; + + g_cancellable_cancel (global->switcheroo_cancellable); + g_clear_object (&global->switcheroo_cancellable); + + g_clear_object (&global->userdatadir_path); + g_clear_object (&global->runtime_state_path); + + g_free (global->session_mode); + g_free (global->imagedir); + g_free (global->userdatadir); + + g_hash_table_unref (global->save_ops); + + G_OBJECT_CLASS(shell_global_parent_class)->finalize (object); +} + +static void +shell_global_class_init (ShellGlobalClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_global_get_property; + gobject_class->set_property = shell_global_set_property; + gobject_class->finalize = shell_global_finalize; + + shell_global_signals[NOTIFY_ERROR] = + g_signal_new ("notify-error", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_STRING, + G_TYPE_STRING); + shell_global_signals[LOCATE_POINTER] = + g_signal_new ("locate-pointer", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, 0); + + props[PROP_SESSION_MODE] = + g_param_spec_string ("session-mode", + "Session Mode", + "The session mode to use", + "user", + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); + + props[PROP_SCREEN_WIDTH] = + g_param_spec_int ("screen-width", + "Screen Width", + "Screen width, in pixels", + 0, G_MAXINT, 1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_SCREEN_HEIGHT] = + g_param_spec_int ("screen-height", + "Screen Height", + "Screen height, in pixels", + 0, G_MAXINT, 1, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_BACKEND] = + g_param_spec_object ("backend", + "Backend", + "MetaBackend object", + META_TYPE_BACKEND, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_CONTEXT] = + g_param_spec_object ("context", + "Context", + "MetaContext object", + META_TYPE_CONTEXT, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_DISPLAY] = + g_param_spec_object ("display", + "Display", + "Metacity display object for the shell", + META_TYPE_DISPLAY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WORKSPACE_MANAGER] = + g_param_spec_object ("workspace-manager", + "Workspace manager", + "Workspace manager", + META_TYPE_WORKSPACE_MANAGER, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_STAGE] = + g_param_spec_object ("stage", + "Stage", + "Stage holding the desktop scene graph", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WINDOW_GROUP] = + g_param_spec_object ("window-group", + "Window Group", + "Actor holding window actors", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_TOP_WINDOW_GROUP] = + g_param_spec_object ("top-window-group", + "Top Window Group", + "Actor holding override-redirect windows", + CLUTTER_TYPE_ACTOR, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_WINDOW_MANAGER] = + g_param_spec_object ("window-manager", + "Window Manager", + "Window management interface", + SHELL_TYPE_WM, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_SETTINGS] = + g_param_spec_object ("settings", + "Settings", + "GSettings instance for gnome-shell configuration", + G_TYPE_SETTINGS, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_DATADIR] = + g_param_spec_string ("datadir", + "Data directory", + "Directory containing gnome-shell data files", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_IMAGEDIR] = + g_param_spec_string ("imagedir", + "Image directory", + "Directory containing gnome-shell image files", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_USERDATADIR] = + g_param_spec_string ("userdatadir", + "User data directory", + "Directory containing gnome-shell user data", + NULL, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_FOCUS_MANAGER] = + g_param_spec_object ("focus-manager", + "Focus manager", + "The shell's StFocusManager", + ST_TYPE_FOCUS_MANAGER, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + props[PROP_FRAME_TIMESTAMPS] = + g_param_spec_boolean ("frame-timestamps", + "Frame Timestamps", + "Whether to log frame timestamps in the performance log", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_FRAME_FINISH_TIMESTAMP] = + g_param_spec_boolean ("frame-finish-timestamp", + "Frame Finish Timestamps", + "Whether at the end of a frame to call glFinish and log paintCompletedTimestamp", + FALSE, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + props[PROP_SWITCHEROO_CONTROL] = + g_param_spec_object ("switcheroo-control", + "switcheroo-control", + "D-Bus Proxy for switcheroo-control daemon", + G_TYPE_DBUS_PROXY, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + g_object_class_install_properties (gobject_class, N_PROPS, props); +} + +/* + * _shell_global_init: (skip) + * @first_property_name: the name of the first property + * @...: the value of the first property, followed optionally by more + * name/value pairs, followed by %NULL + * + * Initializes the shell global singleton with the construction-time + * properties. + * + * There are currently no such properties, so @first_property_name should + * always be %NULL. + * + * This call must be called before shell_global_get() and shouldn't be called + * more than once. + */ +void +_shell_global_init (const char *first_property_name, + ...) +{ + va_list argument_list; + + g_return_if_fail (the_object == NULL); + + va_start (argument_list, first_property_name); + the_object = SHELL_GLOBAL (g_object_new_valist (SHELL_TYPE_GLOBAL, + first_property_name, + argument_list)); + va_end (argument_list); + +} + +/** + * shell_global_get: + * + * Gets the singleton global object that represents the desktop. + * + * Return value: (transfer none): the singleton global object + */ +ShellGlobal * +shell_global_get (void) +{ + return the_object; +} + +/** + * _shell_global_destroy_gjs_context: (skip) + * @self: global object + * + * Destroys the GjsContext held by ShellGlobal, in order to break reference + * counting cycles. (The GjsContext holds a reference to ShellGlobal because + * it's available as window.global inside JS.) + */ +void +_shell_global_destroy_gjs_context (ShellGlobal *self) +{ + g_clear_object (&self->js_context); +} + +static guint32 +get_current_time_maybe_roundtrip (ShellGlobal *global) +{ + guint32 time; + + time = shell_global_get_current_time (global); + if (time != CurrentTime) + return time; + + return meta_display_get_current_time_roundtrip (global->meta_display); +} + +static void +focus_window_changed (MetaDisplay *display, + GParamSpec *param, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + /* If the stage window became unfocused, drop the key focus + * on Clutter's side. */ + if (!meta_stage_is_focused (global->meta_display)) + clutter_stage_set_key_focus (global->stage, NULL); +} + +static ClutterActor * +get_key_focused_actor (ShellGlobal *global) +{ + ClutterActor *actor; + + actor = clutter_stage_get_key_focus (global->stage); + + /* If there's no explicit key focus, clutter_stage_get_key_focus() + * returns the stage. This is a terrible API. */ + if (actor == CLUTTER_ACTOR (global->stage)) + actor = NULL; + + return actor; +} + +static void +sync_stage_window_focus (ShellGlobal *global) +{ + ClutterActor *actor; + + actor = get_key_focused_actor (global); + + /* An actor got key focus and the stage needs to be focused. */ + if (actor != NULL && !meta_stage_is_focused (global->meta_display)) + meta_focus_stage_window (global->meta_display, + get_current_time_maybe_roundtrip (global)); + + /* An actor dropped key focus. Focus the default window. */ + else if (actor == NULL && meta_stage_is_focused (global->meta_display)) + meta_display_focus_default_window (global->meta_display, + get_current_time_maybe_roundtrip (global)); +} + +static void +focus_actor_changed (ClutterStage *stage, + GParamSpec *param, + gpointer user_data) +{ + ShellGlobal *global = user_data; + sync_stage_window_focus (global); +} + +static void +sync_input_region (ShellGlobal *global) +{ + MetaDisplay *display = global->meta_display; + MetaX11Display *x11_display = meta_display_get_x11_display (display); + + meta_x11_display_set_stage_input_region (x11_display, global->input_region); +} + +/** + * shell_global_set_stage_input_region: + * @global: the #ShellGlobal + * @rectangles: (element-type Meta.Rectangle): a list of #MetaRectangle + * describing the input region. + * + * Sets the area of the stage that is responsive to mouse clicks when + * we don't have a modal or grab. + */ +void +shell_global_set_stage_input_region (ShellGlobal *global, + GSList *rectangles) +{ + MetaRectangle *rect; + XRectangle *rects; + int nrects, i; + GSList *r; + + g_return_if_fail (SHELL_IS_GLOBAL (global)); + + if (meta_is_wayland_compositor ()) + return; + + nrects = g_slist_length (rectangles); + rects = g_new (XRectangle, nrects); + for (r = rectangles, i = 0; r; r = r->next, i++) + { + rect = (MetaRectangle *)r->data; + rects[i].x = rect->x; + rects[i].y = rect->y; + rects[i].width = rect->width; + rects[i].height = rect->height; + } + + if (global->input_region) + XFixesDestroyRegion (global->xdisplay, global->input_region); + + global->input_region = XFixesCreateRegion (global->xdisplay, rects, nrects); + g_free (rects); + + sync_input_region (global); +} + +/** + * shell_global_get_stage: + * + * Return value: (transfer none): The default #ClutterStage + */ +ClutterStage * +shell_global_get_stage (ShellGlobal *global) +{ + return global->stage; +} + +/** + * shell_global_get_display: + * + * Return value: (transfer none): The default #MetaDisplay + */ +MetaDisplay * +shell_global_get_display (ShellGlobal *global) +{ + return global->meta_display; +} + +/** + * shell_global_get_workspace_manager: + * + * Return value: (transfer none): The default #MetaWorkspaceManager + */ +MetaWorkspaceManager * +shell_global_get_workspace_manager (ShellGlobal *global) +{ + return global->workspace_manager; +} + +/** + * shell_global_get_window_actors: + * + * Gets the list of #MetaWindowActor for the plugin's screen + * + * Return value: (element-type Meta.WindowActor) (transfer container): the list of windows + */ +GList * +shell_global_get_window_actors (ShellGlobal *global) +{ + GList *filtered = NULL; + GList *l; + + g_return_val_if_fail (SHELL_IS_GLOBAL (global), NULL); + + for (l = meta_get_window_actors (global->meta_display); l; l = l->next) + if (!meta_window_actor_is_destroyed (l->data)) + filtered = g_list_prepend (filtered, l->data); + + return g_list_reverse (filtered); +} + +static void +global_stage_notify_width (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_WIDTH]); +} + +static void +global_stage_notify_height (GObject *gobject, + GParamSpec *pspec, + gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + g_object_notify_by_pspec (G_OBJECT (global), props[PROP_SCREEN_HEIGHT]); +} + +static gboolean +global_stage_before_paint (gpointer data) +{ + ShellGlobal *global = SHELL_GLOBAL (data); + + if (global->frame_timestamps) + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.stagePaintStart"); + + return TRUE; +} + +static gboolean +load_gl_symbol (const char *name, + void **func) +{ + *func = cogl_get_proc_address (name); + if (!*func) + { + g_warning ("failed to resolve required GL symbol \"%s\"\n", name); + return FALSE; + } + return TRUE; +} + +static void +global_stage_after_paint (ClutterStage *stage, + ClutterStageView *stage_view, + ShellGlobal *global) +{ + /* At this point, we've finished all layout and painting, but haven't + * actually flushed or swapped */ + + if (global->frame_timestamps && global->frame_finish_timestamp) + { + /* It's interesting to find out when the paint actually finishes + * on the GPU. We could wait for this asynchronously with + * ARB_timer_query (see https://bugzilla.gnome.org/show_bug.cgi?id=732350 + * for an implementation of this), but what we actually would + * find out then is the latency for drawing a frame, not how much + * GPU work was needed, since frames can overlap. Calling glFinish() + * is a fairly reliable way to separate out adjacent frames + * and measure the amount of GPU work. This is turned on with a + * separate property from ::frame-timestamps, since it should not + * be turned on if we're trying to actual measure latency or frame + * rate. + */ + static void (*finish) (void); + + if (!finish) + load_gl_symbol ("glFinish", (void **)&finish); + + cogl_flush (); + finish (); + + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.paintCompletedTimestamp"); + } +} + +static gboolean +global_stage_after_swap (gpointer data) +{ + /* Everything is done, we're ready for a new frame */ + + ShellGlobal *global = SHELL_GLOBAL (data); + + if (global->frame_timestamps) + shell_perf_log_event (shell_perf_log_get_default (), + "clutter.stagePaintDone"); + + return TRUE; +} + +static void +update_scaling_factor (ShellGlobal *global, + MetaSettings *settings) +{ + ClutterStage *stage = CLUTTER_STAGE (global->stage); + StThemeContext *context = st_theme_context_get_for_stage (stage); + int scaling_factor; + + scaling_factor = meta_settings_get_ui_scaling_factor (settings); + g_object_set (context, "scale-factor", scaling_factor, NULL); +} + +static void +ui_scaling_factor_changed (MetaSettings *settings, + ShellGlobal *global) +{ + update_scaling_factor (global, settings); +} + +static void +entry_cursor_func (StEntry *entry, + gboolean use_ibeam, + gpointer user_data) +{ + ShellGlobal *global = user_data; + + meta_display_set_cursor (global->meta_display, + use_ibeam ? META_CURSOR_IBEAM : META_CURSOR_DEFAULT); +} + +static void +on_x11_display_closed (MetaDisplay *display, + ShellGlobal *global) +{ + g_signal_handlers_disconnect_by_data (global->stage, global); +} + +void +_shell_global_set_plugin (ShellGlobal *global, + MetaPlugin *plugin) +{ + MetaDisplay *display; + MetaBackend *backend; + MetaSettings *settings; + + g_return_if_fail (SHELL_IS_GLOBAL (global)); + g_return_if_fail (global->plugin == NULL); + + global->backend = meta_get_backend (); + global->plugin = plugin; + global->wm = shell_wm_new (plugin); + + display = meta_plugin_get_display (plugin); + global->meta_display = display; + global->meta_context = meta_display_get_context (display); + global->workspace_manager = meta_display_get_workspace_manager (display); + + global->stage = CLUTTER_STAGE (meta_get_stage_for_display (display)); + + if (!meta_is_wayland_compositor ()) + { + MetaX11Display *x11_display = meta_display_get_x11_display (display); + global->xdisplay = meta_x11_display_get_xdisplay (x11_display); + } + + st_entry_set_cursor_func (entry_cursor_func, global); + st_clipboard_set_selection (meta_display_get_selection (display)); + + g_signal_connect (global->stage, "notify::width", + G_CALLBACK (global_stage_notify_width), global); + g_signal_connect (global->stage, "notify::height", + G_CALLBACK (global_stage_notify_height), global); + + clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_PRE_PAINT, + global_stage_before_paint, + global, NULL); + + g_signal_connect (global->stage, "after-paint", + G_CALLBACK (global_stage_after_paint), global); + + clutter_threads_add_repaint_func_full (CLUTTER_REPAINT_FLAGS_POST_PAINT, + global_stage_after_swap, + global, NULL); + + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.stagePaintStart", + "Start of stage page repaint", + ""); + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.paintCompletedTimestamp", + "Paint completion on GPU", + ""); + shell_perf_log_define_event (shell_perf_log_get_default(), + "clutter.stagePaintDone", + "End of frame, possibly including swap time", + ""); + + g_signal_connect (global->stage, "notify::key-focus", + G_CALLBACK (focus_actor_changed), global); + g_signal_connect (global->meta_display, "notify::focus-window", + G_CALLBACK (focus_window_changed), global); + + if (global->xdisplay) + g_signal_connect_object (global->meta_display, "x11-display-closing", + G_CALLBACK (on_x11_display_closed), global, 0); + + backend = meta_get_backend (); + settings = meta_backend_get_settings (backend); + g_signal_connect (settings, "ui-scaling-factor-changed", + G_CALLBACK (ui_scaling_factor_changed), global); + + global->focus_manager = st_focus_manager_get_for_stage (global->stage); + + update_scaling_factor (global, settings); +} + +GjsContext * +_shell_global_get_gjs_context (ShellGlobal *global) +{ + return global->js_context; +} + +/* Code to close all file descriptors before we exec; copied from gspawn.c in GLib. + * + * Authors: Padraig O'Briain, Matthias Clasen, Lennart Poettering + * + * http://bugzilla.gnome.org/show_bug.cgi?id=469231 + * http://bugzilla.gnome.org/show_bug.cgi?id=357585 + */ + +static int +set_cloexec (void *data, gint fd) +{ + if (fd >= GPOINTER_TO_INT (data)) + fcntl (fd, F_SETFD, FD_CLOEXEC); + + return 0; +} + +#ifndef HAVE_FDWALK +static int +fdwalk (int (*cb)(void *data, int fd), void *data) +{ + gint open_max; + gint fd; + gint res = 0; + +#ifdef HAVE_SYS_RESOURCE_H + struct rlimit rl; +#endif + +#ifdef __linux__ + DIR *d; + + if ((d = opendir("/proc/self/fd"))) { + struct dirent *de; + + while ((de = readdir(d))) { + glong l; + gchar *e = NULL; + + if (de->d_name[0] == '.') + continue; + + errno = 0; + l = strtol(de->d_name, &e, 10); + if (errno != 0 || !e || *e) + continue; + + fd = (gint) l; + + if ((glong) fd != l) + continue; + + if (fd == dirfd(d)) + continue; + + if ((res = cb (data, fd)) != 0) + break; + } + + closedir(d); + return res; + } + + /* If /proc is not mounted or not accessible we fall back to the old + * rlimit trick */ + +#endif + +#ifdef HAVE_SYS_RESOURCE_H + if (getrlimit(RLIMIT_NOFILE, &rl) == 0 && rl.rlim_max != RLIM_INFINITY) + open_max = rl.rlim_max; + else +#endif + open_max = sysconf (_SC_OPEN_MAX); + + for (fd = 0; fd < open_max; fd++) + if ((res = cb (data, fd)) != 0) + break; + + return res; +} +#endif + +static void +pre_exec_close_fds(void) +{ + fdwalk (set_cloexec, GINT_TO_POINTER(3)); +} + +/** + * shell_global_reexec_self: + * @global: A #ShellGlobal + * + * Restart the current process. Only intended for development purposes. + */ +void +shell_global_reexec_self (ShellGlobal *global) +{ + GPtrArray *arr; + gsize len; + MetaContext *meta_context; + +#if defined __linux__ || defined __sun + char *buf; + char *buf_p; + char *buf_end; + g_autoptr (GError) error = NULL; + + if (!g_file_get_contents ("/proc/self/cmdline", &buf, &len, &error)) + { + g_warning ("failed to get /proc/self/cmdline: %s", error->message); + return; + } + + buf_end = buf+len; + arr = g_ptr_array_new (); + /* The cmdline file is NUL-separated */ + for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) + g_ptr_array_add (arr, buf_p); + + g_ptr_array_add (arr, NULL); +#elif defined __OpenBSD__ + gchar **args, **args_p; + gint mib[] = { CTL_KERN, KERN_PROC_ARGS, getpid(), KERN_PROC_ARGV }; + + if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) + return; + + args = g_malloc0 (len); + + if (sysctl (mib, G_N_ELEMENTS (mib), args, &len, NULL, 0) == -1) { + g_warning ("failed to get command line args: %d", errno); + g_free (args); + return; + } + + arr = g_ptr_array_new (); + for (args_p = args; *args_p != NULL; args_p++) { + g_ptr_array_add (arr, *args_p); + } + + g_ptr_array_add (arr, NULL); +#elif defined __FreeBSD__ + char *buf; + char *buf_p; + char *buf_end; + gint mib[] = { CTL_KERN, KERN_PROC, KERN_PROC_ARGS, getpid() }; + + if (sysctl (mib, G_N_ELEMENTS (mib), NULL, &len, NULL, 0) == -1) + return; + + buf = g_malloc0 (len); + + if (sysctl (mib, G_N_ELEMENTS (mib), buf, &len, NULL, 0) == -1) { + g_warning ("failed to get command line args: %d", errno); + g_free (buf); + return; + } + + buf_end = buf+len; + arr = g_ptr_array_new (); + /* The value returned by sysctl is NUL-separated */ + for (buf_p = buf; buf_p < buf_end; buf_p = buf_p + strlen (buf_p) + 1) + g_ptr_array_add (arr, buf_p); + + g_ptr_array_add (arr, NULL); +#else + return; +#endif + + /* Close all file descriptors other than stdin/stdout/stderr, otherwise + * they will leak and stay open after the exec. In particular, this is + * important for file descriptors that represent mapped graphics buffer + * objects. + */ + pre_exec_close_fds (); + + g_object_get (global, "context", &meta_context, NULL); + meta_context_restore_rlimit_nofile (meta_context, NULL); + + meta_display_close (shell_global_get_display (global), + shell_global_get_current_time (global)); + + execvp (arr->pdata[0], (char**)arr->pdata); + g_warning ("failed to reexec: %s", g_strerror (errno)); + g_ptr_array_free (arr, TRUE); +#if defined __linux__ || defined __FreeBSD__ + g_free (buf); +#elif defined __OpenBSD__ + g_free (args); +#endif +} + +/** + * shell_global_notify_error: + * @global: a #ShellGlobal + * @msg: Error message + * @details: Error details + * + * Show a system error notification. Use this function + * when a user-initiated action results in a non-fatal problem + * from causes that may not be under system control. For + * example, an application crash. + */ +void +shell_global_notify_error (ShellGlobal *global, + const char *msg, + const char *details) +{ + g_signal_emit_by_name (global, "notify-error", msg, details); +} + +/** + * shell_global_get_pointer: + * @global: the #ShellGlobal + * @x: (out): the X coordinate of the pointer, in global coordinates + * @y: (out): the Y coordinate of the pointer, in global coordinates + * @mods: (out): the current set of modifier keys that are pressed down + * + * Gets the pointer coordinates and current modifier key state. + */ +void +shell_global_get_pointer (ShellGlobal *global, + int *x, + int *y, + ClutterModifierType *mods) +{ + ClutterModifierType raw_mods; + MetaCursorTracker *tracker; + graphene_point_t point; + + tracker = meta_cursor_tracker_get_for_display (global->meta_display); + meta_cursor_tracker_get_pointer (tracker, &point, &raw_mods); + + if (x) + *x = point.x; + if (y) + *y = point.y; + + *mods = raw_mods & CLUTTER_MODIFIER_MASK; +} + +/** + * shell_global_get_switcheroo_control: + * @global: A #ShellGlobal + * + * Get the global #GDBusProxy instance for the switcheroo-control + * daemon. + * + * Return value: (transfer none): the #GDBusProxy for the daemon, + * or %NULL on error. + */ +GDBusProxy * +shell_global_get_switcheroo_control (ShellGlobal *global) +{ + return global->switcheroo_control; +} + +/** + * shell_global_get_settings: + * @global: A #ShellGlobal + * + * Get the global GSettings instance. + * + * Return value: (transfer none): The GSettings object + */ +GSettings * +shell_global_get_settings (ShellGlobal *global) +{ + return global->settings; +} + +/** + * shell_global_get_current_time: + * @global: A #ShellGlobal + * + * Returns: the current X server time from the current Clutter, Gdk, or X + * event. If called from outside an event handler, this may return + * %Clutter.CURRENT_TIME (aka 0), or it may return a slightly + * out-of-date timestamp. + */ +guint32 +shell_global_get_current_time (ShellGlobal *global) +{ + guint32 time; + + /* meta_display_get_current_time() will return the correct time + when handling an X or Gdk event, but will return CurrentTime + from some Clutter event callbacks. + + clutter_get_current_event_time() will return the correct time + from a Clutter event callback, but may return CLUTTER_CURRENT_TIME + timestamp if called at other times. + + So we try meta_display_get_current_time() first, since we + can recognize a "wrong" answer from that, and then fall back + to clutter_get_current_event_time(). + */ + + time = meta_display_get_current_time (global->meta_display); + if (time != CLUTTER_CURRENT_TIME) + return time; + + return clutter_get_current_event_time (); +} + +static void +shell_global_app_launched_cb (GAppLaunchContext *context, + GAppInfo *info, + GVariant *platform_data, + gpointer user_data) +{ + gint32 pid; + const gchar *app_name; + + if (!g_variant_lookup (platform_data, "pid", "i", &pid)) + return; + + /* If pid == 0 the application was launched through D-Bus + * activation, therefore it's already in its own unit */ + if (pid == 0) + return; + + app_name = g_app_info_get_id (info); + if (app_name == NULL) + app_name = g_app_info_get_executable (info); + + /* Start async request; we don't care about the result */ + gnome_start_systemd_scope (app_name, + pid, + NULL, + NULL, + NULL, NULL, NULL); +} + +/** + * shell_global_create_app_launch_context: + * @global: A #ShellGlobal + * @timestamp: the timestamp for the launch (or 0 for current time) + * @workspace: a workspace index, or -1 to indicate the current one + * + * Create a #GAppLaunchContext set up with the correct timestamp, and + * targeted to activate on the current workspace. + * + * Return value: (transfer full): A new #GAppLaunchContext + */ +GAppLaunchContext * +shell_global_create_app_launch_context (ShellGlobal *global, + guint32 timestamp, + int workspace) +{ + MetaWorkspaceManager *workspace_manager = global->workspace_manager; + MetaStartupNotification *sn; + MetaLaunchContext *context; + MetaWorkspace *ws = NULL; + + sn = meta_display_get_startup_notification (global->meta_display); + context = meta_startup_notification_create_launcher (sn); + + if (timestamp == 0) + timestamp = shell_global_get_current_time (global); + meta_launch_context_set_timestamp (context, timestamp); + + if (workspace < 0) + ws = meta_workspace_manager_get_active_workspace (workspace_manager); + else + ws = meta_workspace_manager_get_workspace_by_index (workspace_manager, workspace); + + meta_launch_context_set_workspace (context, ws); + + g_signal_connect (context, + "launched", + G_CALLBACK (shell_global_app_launched_cb), + NULL); + + return (GAppLaunchContext *) context; +} + +typedef struct +{ + ShellLeisureFunction func; + gpointer user_data; + GDestroyNotify notify; +} LeisureClosure; + +static gboolean +run_leisure_functions (gpointer data) +{ + ShellGlobal *global = data; + GSList *closures; + GSList *iter; + + global->leisure_function_id = 0; + + /* We started more work since we scheduled the idle */ + if (global->work_count > 0) + return FALSE; + + /* No leisure closures, so we are done */ + if (global->leisure_closures == NULL) + return FALSE; + + closures = global->leisure_closures; + global->leisure_closures = NULL; + + for (iter = closures; iter; iter = iter->next) + { + LeisureClosure *closure = closures->data; + closure->func (closure->user_data); + + if (closure->notify) + closure->notify (closure->user_data); + + g_free (closure); + } + + g_slist_free (closures); + + return FALSE; +} + +static void +schedule_leisure_functions (ShellGlobal *global) +{ + /* This is called when we think we are ready to run leisure functions + * by our own accounting. We try to handle other types of business + * (like ClutterAnimation) by adding a low priority idle function. + * + * This won't work properly if the mainloop goes idle waiting for + * the vertical blanking interval or waiting for work being done + * in another thread. + */ + if (!global->leisure_function_id) + { + global->leisure_function_id = g_idle_add_full (G_PRIORITY_LOW, + run_leisure_functions, + global, NULL); + g_source_set_name_by_id (global->leisure_function_id, "[gnome-shell] run_leisure_functions"); + } +} + +/** + * shell_global_begin_work: + * @global: the #ShellGlobal + * + * Marks that we are currently doing work. This is used to to track + * whether we are busy for the purposes of shell_global_run_at_leisure(). + * A count is kept and shell_global_end_work() must be called exactly + * as many times as shell_global_begin_work(). + */ +void +shell_global_begin_work (ShellGlobal *global) +{ + global->work_count++; +} + +/** + * shell_global_end_work: + * @global: the #ShellGlobal + * + * Marks the end of work that we started with shell_global_begin_work(). + * If no other work is ongoing and functions have been added with + * shell_global_run_at_leisure(), they will be run at the next + * opportunity. + */ +void +shell_global_end_work (ShellGlobal *global) +{ + g_return_if_fail (global->work_count > 0); + + global->work_count--; + if (global->work_count == 0) + schedule_leisure_functions (global); + +} + +/** + * shell_global_run_at_leisure: + * @global: the #ShellGlobal + * @func: function to call at leisure + * @user_data: data to pass to @func + * @notify: function to call to free @user_data + * + * Schedules a function to be called the next time the shell is idle. + * Idle means here no animations, no redrawing, and no ongoing background + * work. Since there is currently no way to hook into the Clutter master + * clock and know when is running, the implementation here is somewhat + * approximation. Animations may be detected as terminating early if they + * can be drawn fast enough so that the event loop goes idle between frames. + * + * The intent of this function is for performance measurement runs + * where a number of actions should be run serially and each action is + * timed individually. Using this function for other purposes will + * interfere with the ability to use it for performance measurement so + * should be avoided. + */ +void +shell_global_run_at_leisure (ShellGlobal *global, + ShellLeisureFunction func, + gpointer user_data, + GDestroyNotify notify) +{ + LeisureClosure *closure = g_new (LeisureClosure, 1); + closure->func = func; + closure->user_data = user_data; + closure->notify = notify; + + global->leisure_closures = g_slist_append (global->leisure_closures, + closure); + + if (global->work_count == 0) + schedule_leisure_functions (global); +} + +const char * +shell_global_get_session_mode (ShellGlobal *global) +{ + g_return_val_if_fail (SHELL_IS_GLOBAL (global), "user"); + + return global->session_mode; +} + +static void +delete_variant_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellGlobal *global = user_data; + GError *error = NULL; + + if (!g_file_delete_finish (G_FILE (object), result, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND)) + { + g_warning ("Could not delete runtime/persistent state file: %s\n", + error->message); + } + + g_error_free (error); + } + + g_hash_table_remove (global->save_ops, object); +} + +static void +replace_contents_worker (GTask *task, + gpointer source_object, + gpointer task_data, + GCancellable *cancellable) +{ + GFile *file = source_object; + GBytes *bytes = task_data; + GError *error = NULL; + const gchar *data; + gsize len; + + data = g_bytes_get_data (bytes, &len); + + if (!g_file_replace_contents (file, data, len, NULL, FALSE, + G_FILE_CREATE_REPLACE_DESTINATION, + NULL, cancellable, &error)) + g_task_return_error (task, g_steal_pointer (&error)); + else + g_task_return_boolean (task, TRUE); +} + +static void +replace_contents_async (GFile *path, + GBytes *bytes, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + g_autoptr(GTask) task = NULL; + + g_assert (G_IS_FILE (path)); + g_assert (bytes != NULL); + g_assert (!cancellable || G_IS_CANCELLABLE (cancellable)); + + task = g_task_new (path, cancellable, callback, user_data); + g_task_set_source_tag (task, replace_contents_async); + g_task_set_task_data (task, g_bytes_ref (bytes), (GDestroyNotify)g_bytes_unref); + g_task_run_in_thread (task, replace_contents_worker); +} + +static gboolean +replace_contents_finish (GFile *file, + GAsyncResult *result, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (result), error); +} + +static void +replace_variant_cb (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + ShellGlobal *global = user_data; + GError *error = NULL; + + if (!replace_contents_finish (G_FILE (object), result, &error)) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Could not replace runtime/persistent state file: %s\n", + error->message); + } + + g_error_free (error); + } + + g_hash_table_remove (global->save_ops, object); +} + +static void +save_variant (ShellGlobal *global, + GFile *dir, + const char *property_name, + GVariant *variant) +{ + GFile *path = g_file_get_child (dir, property_name); + GCancellable *cancellable; + + cancellable = g_hash_table_lookup (global->save_ops, path); + g_cancellable_cancel (cancellable); + + cancellable = g_cancellable_new (); + g_hash_table_insert (global->save_ops, g_object_ref (path), cancellable); + + if (variant == NULL || g_variant_get_data (variant) == NULL) + { + g_file_delete_async (path, G_PRIORITY_DEFAULT, cancellable, + delete_variant_cb, global); + } + else + { + g_autoptr(GBytes) bytes = NULL; + + bytes = g_bytes_new_with_free_func (g_variant_get_data (variant), + g_variant_get_size (variant), + (GDestroyNotify)g_variant_unref, + g_variant_ref (variant)); + /* g_file_replace_contents_async() can potentially fsync() from the + * calling thread when completing the asynchronous task. Instead, we + * want to force that fsync() to a thread to avoid blocking the + * compositor main loop. Using our own replace_contents_async() + * simply executes the operation synchronously from a thread. + */ + replace_contents_async (path, bytes, cancellable, replace_variant_cb, global); + } + + g_object_unref (path); +} + +static GVariant * +load_variant (GFile *dir, + const char *property_type, + const char *property_name) +{ + GVariant *res = NULL; + GMappedFile *mfile; + GFile *path = g_file_get_child (dir, property_name); + char *pathstr; + GError *local_error = NULL; + + pathstr = g_file_get_path (path); + mfile = g_mapped_file_new (pathstr, FALSE, &local_error); + if (!mfile) + { + if (!g_error_matches (local_error, G_FILE_ERROR, G_FILE_ERROR_NOENT)) + { + g_warning ("Failed to open runtime state: %s", local_error->message); + } + g_clear_error (&local_error); + } + else + { + GBytes *bytes = g_mapped_file_get_bytes (mfile); + res = g_variant_new_from_bytes (G_VARIANT_TYPE (property_type), bytes, FALSE); + g_bytes_unref (bytes); + g_mapped_file_unref (mfile); + } + + g_object_unref (path); + g_free (pathstr); + + return res; +} + +/** + * shell_global_set_runtime_state: + * @global: a #ShellGlobal + * @property_name: Name of the property + * @variant: (nullable): A #GVariant, or %NULL to unset + * + * Change the value of serialized runtime state. + */ +void +shell_global_set_runtime_state (ShellGlobal *global, + const char *property_name, + GVariant *variant) +{ + save_variant (global, global->runtime_state_path, property_name, variant); +} + +/** + * shell_global_get_runtime_state: + * @global: a #ShellGlobal + * @property_type: Expected data type + * @property_name: Name of the property + * + * The shell maintains "runtime" state which does not persist across + * logout or reboot. + * + * Returns: (transfer floating): The value of a serialized property, or %NULL if none stored + */ +GVariant * +shell_global_get_runtime_state (ShellGlobal *global, + const char *property_type, + const char *property_name) +{ + return load_variant (global->runtime_state_path, property_type, property_name); +} + +/** + * shell_global_set_persistent_state: + * @global: a #ShellGlobal + * @property_name: Name of the property + * @variant: (nullable): A #GVariant, or %NULL to unset + * + * Change the value of serialized persistent state. + */ +void +shell_global_set_persistent_state (ShellGlobal *global, + const char *property_name, + GVariant *variant) +{ + save_variant (global, global->userdatadir_path, property_name, variant); +} + +/** + * shell_global_get_persistent_state: + * @global: a #ShellGlobal + * @property_type: Expected data type + * @property_name: Name of the property + * + * The shell maintains "persistent" state which will persist after + * logout or reboot. + * + * Returns: (transfer none): The value of a serialized property, or %NULL if none stored + */ +GVariant * +shell_global_get_persistent_state (ShellGlobal *global, + const char *property_type, + const char *property_name) +{ + return load_variant (global->userdatadir_path, property_type, property_name); +} + +void +_shell_global_locate_pointer (ShellGlobal *global) +{ + g_signal_emit (global, shell_global_signals[LOCATE_POINTER], 0); +} diff --git a/src/shell-global.h b/src/shell-global.h new file mode 100644 index 0000000..8d8238c --- /dev/null +++ b/src/shell-global.h @@ -0,0 +1,94 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLOBAL_H__ +#define __SHELL_GLOBAL_H__ + +#include +#include +#include +#include +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_GLOBAL (shell_global_get_type ()) +G_DECLARE_FINAL_TYPE (ShellGlobal, shell_global, SHELL, GLOBAL, GObject) + +ShellGlobal *shell_global_get (void); + +ClutterStage *shell_global_get_stage (ShellGlobal *global); +MetaDisplay *shell_global_get_display (ShellGlobal *global); +GList *shell_global_get_window_actors (ShellGlobal *global); +GSettings *shell_global_get_settings (ShellGlobal *global); +guint32 shell_global_get_current_time (ShellGlobal *global); +MetaWorkspaceManager *shell_global_get_workspace_manager (ShellGlobal *global); + + +/* Input/event handling */ +void shell_global_set_stage_input_region (ShellGlobal *global, + GSList *rectangles); + +void shell_global_get_pointer (ShellGlobal *global, + int *x, + int *y, + ClutterModifierType *mods); + +typedef struct { + guint glibc_uordblks; + + guint js_bytes; + + guint gjs_boxed; + guint gjs_gobject; + guint gjs_function; + guint gjs_closure; + + /* 32 bit to avoid js conversion problems with 64 bit */ + guint last_gc_seconds_ago; +} ShellMemoryInfo; + +/* Run-at-leisure API */ +void shell_global_begin_work (ShellGlobal *global); +void shell_global_end_work (ShellGlobal *global); + +typedef void (*ShellLeisureFunction) (gpointer data); + +void shell_global_run_at_leisure (ShellGlobal *global, + ShellLeisureFunction func, + gpointer user_data, + GDestroyNotify notify); + + +/* Misc utilities / Shell API */ +GDBusProxy * + shell_global_get_switcheroo_control (ShellGlobal *global); + +GAppLaunchContext * + shell_global_create_app_launch_context (ShellGlobal *global, + guint32 timestamp, + int workspace); + +void shell_global_notify_error (ShellGlobal *global, + const char *msg, + const char *details); + +void shell_global_reexec_self (ShellGlobal *global); + +const char * shell_global_get_session_mode (ShellGlobal *global); + +void shell_global_set_runtime_state (ShellGlobal *global, + const char *property_name, + GVariant *variant); +GVariant * shell_global_get_runtime_state (ShellGlobal *global, + const char *property_type, + const char *property_name); + +void shell_global_set_persistent_state (ShellGlobal *global, + const char *property_name, + GVariant *variant); +GVariant * shell_global_get_persistent_state (ShellGlobal *global, + const char *property_type, + const char *property_name); + +G_END_DECLS + +#endif /* __SHELL_GLOBAL_H__ */ diff --git a/src/shell-glsl-effect.c b/src/shell-glsl-effect.c new file mode 100644 index 0000000..3051e0a --- /dev/null +++ b/src/shell-glsl-effect.c @@ -0,0 +1,205 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/** + * SECTION:shell-glsl-effect + * @short_description: An offscreen effect using GLSL + * + * A #ShellGLSLEffect is a #ClutterOffscreenEffect that allows + * running custom GLSL to the vertex and fragment stages of the + * graphic pipeline. + */ + +#include "config.h" + +#include +#include "shell-glsl-effect.h" + +typedef struct _ShellGLSLEffectPrivate ShellGLSLEffectPrivate; +struct _ShellGLSLEffectPrivate +{ + CoglPipeline *pipeline; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellGLSLEffect, shell_glsl_effect, CLUTTER_TYPE_OFFSCREEN_EFFECT); + +static CoglPipeline * +shell_glsl_effect_create_pipeline (ClutterOffscreenEffect *effect, + CoglTexture *texture) +{ + ShellGLSLEffect *self = SHELL_GLSL_EFFECT (effect); + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (self); + + cogl_pipeline_set_layer_texture (priv->pipeline, 0, texture); + + return cogl_object_ref (priv->pipeline); +} + +/** + * shell_glsl_effect_add_glsl_snippet: + * @effect: a #ShellGLSLEffect + * @hook: where to insert the code + * @declarations: GLSL declarations + * @code: GLSL code + * @is_replace: whether Cogl code should be replaced by the custom shader + * + * Adds a GLSL snippet to the pipeline used for drawing the effect texture. + * See #CoglSnippet for details. + * + * This is only valid inside the a call to the build_pipeline() virtual + * function. + */ +void +shell_glsl_effect_add_glsl_snippet (ShellGLSLEffect *effect, + ShellSnippetHook hook, + const char *declarations, + const char *code, + gboolean is_replace) +{ + ShellGLSLEffectClass *klass = SHELL_GLSL_EFFECT_GET_CLASS (effect); + CoglSnippet *snippet; + + g_return_if_fail (klass->base_pipeline != NULL); + + if (is_replace) + { + snippet = cogl_snippet_new ((CoglSnippetHook)hook, declarations, NULL); + cogl_snippet_set_replace (snippet, code); + } + else + { + snippet = cogl_snippet_new ((CoglSnippetHook)hook, declarations, code); + } + + if (hook == SHELL_SNIPPET_HOOK_VERTEX || + hook == SHELL_SNIPPET_HOOK_FRAGMENT) + cogl_pipeline_add_snippet (klass->base_pipeline, snippet); + else + cogl_pipeline_add_layer_snippet (klass->base_pipeline, 0, snippet); + + cogl_object_unref (snippet); +} + +static void +shell_glsl_effect_dispose (GObject *gobject) +{ + ShellGLSLEffect *self = SHELL_GLSL_EFFECT (gobject); + ShellGLSLEffectPrivate *priv; + + priv = shell_glsl_effect_get_instance_private (self); + + g_clear_pointer (&priv->pipeline, cogl_object_unref); + + G_OBJECT_CLASS (shell_glsl_effect_parent_class)->dispose (gobject); +} + +static void +shell_glsl_effect_init (ShellGLSLEffect *effect) +{ +} + +static void +shell_glsl_effect_constructed (GObject *object) +{ + ShellGLSLEffect *self; + ShellGLSLEffectClass *klass; + ShellGLSLEffectPrivate *priv; + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + G_OBJECT_CLASS (shell_glsl_effect_parent_class)->constructed (object); + + /* Note that, differently from ClutterBlurEffect, we are calling + this inside constructed, not init, so klass points to the most-derived + GTypeClass, not ShellGLSLEffectClass. + */ + klass = SHELL_GLSL_EFFECT_GET_CLASS (object); + self = SHELL_GLSL_EFFECT (object); + priv = shell_glsl_effect_get_instance_private (self); + + if (G_UNLIKELY (klass->base_pipeline == NULL)) + { + klass->base_pipeline = cogl_pipeline_new (ctx); + cogl_pipeline_set_blend (klass->base_pipeline, "RGBA = ADD (SRC_COLOR * (SRC_COLOR[A]), DST_COLOR * (1-SRC_COLOR[A]))", NULL); + + if (klass->build_pipeline != NULL) + klass->build_pipeline (self); + } + + priv->pipeline = cogl_pipeline_copy (klass->base_pipeline); + + cogl_pipeline_set_layer_null_texture (klass->base_pipeline, 0); +} + +static void +shell_glsl_effect_class_init (ShellGLSLEffectClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterOffscreenEffectClass *offscreen_class; + + offscreen_class = CLUTTER_OFFSCREEN_EFFECT_CLASS (klass); + offscreen_class->create_pipeline = shell_glsl_effect_create_pipeline; + + gobject_class->constructed = shell_glsl_effect_constructed; + gobject_class->dispose = shell_glsl_effect_dispose; +} + +/** + * shell_glsl_effect_get_uniform_location: + * @effect: a #ShellGLSLEffect + * @name: the uniform name + * + * Returns: the location of the uniform named @name, that can be + * passed to shell_glsl_effect_set_uniform_float(). + */ +int +shell_glsl_effect_get_uniform_location (ShellGLSLEffect *effect, + const char *name) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + return cogl_pipeline_get_uniform_location (priv->pipeline, name); +} + +/** + * shell_glsl_effect_set_uniform_float: + * @effect: a #ShellGLSLEffect + * @uniform: the uniform location (as returned by shell_glsl_effect_get_uniform_location()) + * @n_components: the number of components in the uniform (eg. 3 for a vec3) + * @total_count: the total number of floats in @value + * @value: (array length=total_count): the array of floats to set @uniform + */ +void +shell_glsl_effect_set_uniform_float (ShellGLSLEffect *effect, + int uniform, + int n_components, + int total_count, + const float *value) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + cogl_pipeline_set_uniform_float (priv->pipeline, uniform, + n_components, total_count / n_components, + value); +} + +/** + * shell_glsl_effect_set_uniform_matrix: + * @effect: a #ShellGLSLEffect + * @uniform: the uniform location (as returned by shell_glsl_effect_get_uniform_location()) + * @transpose: Whether to transpose the matrix + * @dimensions: the number of components in the uniform (eg. 3 for a vec3) + * @total_count: the total number of floats in @value + * @value: (array length=total_count): the array of floats to set @uniform + */ +void +shell_glsl_effect_set_uniform_matrix (ShellGLSLEffect *effect, + int uniform, + gboolean transpose, + int dimensions, + int total_count, + const float *value) +{ + ShellGLSLEffectPrivate *priv = shell_glsl_effect_get_instance_private (effect); + cogl_pipeline_set_uniform_matrix (priv->pipeline, uniform, + dimensions, + total_count / (dimensions * dimensions), + transpose, value); +} diff --git a/src/shell-glsl-effect.h b/src/shell-glsl-effect.h new file mode 100644 index 0000000..3759e71 --- /dev/null +++ b/src/shell-glsl-effect.h @@ -0,0 +1,62 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GLSL_EFFECT_H__ +#define __SHELL_GLSL_EFFECT_H__ + +#include "st.h" +#include + +/** + * ShellSnippetHook: + * Temporary hack to work around Cogl not exporting CoglSnippetHook in + * the 1.0 API. Don't use. + */ +typedef enum { + /* Per pipeline vertex hooks */ + SHELL_SNIPPET_HOOK_VERTEX = 0, + SHELL_SNIPPET_HOOK_VERTEX_TRANSFORM, + + /* Per pipeline fragment hooks */ + SHELL_SNIPPET_HOOK_FRAGMENT = 2048, + + /* Per layer vertex hooks */ + SHELL_SNIPPET_HOOK_TEXTURE_COORD_TRANSFORM = 4096, + + /* Per layer fragment hooks */ + SHELL_SNIPPET_HOOK_LAYER_FRAGMENT = 6144, + SHELL_SNIPPET_HOOK_TEXTURE_LOOKUP +} ShellSnippetHook; + +#define SHELL_TYPE_GLSL_EFFECT (shell_glsl_effect_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellGLSLEffect, shell_glsl_effect, + SHELL, GLSL_EFFECT, ClutterOffscreenEffect) + +struct _ShellGLSLEffectClass +{ + ClutterOffscreenEffectClass parent_class; + + CoglPipeline *base_pipeline; + + void (*build_pipeline) (ShellGLSLEffect *effect); +}; + +void shell_glsl_effect_add_glsl_snippet (ShellGLSLEffect *effect, + ShellSnippetHook hook, + const char *declarations, + const char *code, + gboolean is_replace); + +int shell_glsl_effect_get_uniform_location (ShellGLSLEffect *effect, + const char *name); +void shell_glsl_effect_set_uniform_float (ShellGLSLEffect *effect, + int uniform, + int n_components, + int total_count, + const float *value); +void shell_glsl_effect_set_uniform_matrix (ShellGLSLEffect *effect, + int uniform, + gboolean transpose, + int dimensions, + int total_count, + const float *value); + +#endif /* __SHELL_GLSL_EFFECT_H__ */ diff --git a/src/shell-gtk-embed.c b/src/shell-gtk-embed.c new file mode 100644 index 0000000..2ad18a1 --- /dev/null +++ b/src/shell-gtk-embed.c @@ -0,0 +1,364 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include "shell-embedded-window-private.h" +#include "shell-global.h" +#include "shell-util.h" + +#include +#include +#include + +enum { + PROP_0, + + PROP_WINDOW +}; + +typedef struct _ShellGtkEmbedPrivate ShellGtkEmbedPrivate; + +struct _ShellGtkEmbedPrivate +{ + ShellEmbeddedWindow *window; + + ClutterActor *window_actor; + gulong window_actor_destroyed_handler; + + gulong window_created_handler; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellGtkEmbed, shell_gtk_embed, CLUTTER_TYPE_CLONE); + +static void shell_gtk_embed_set_window (ShellGtkEmbed *embed, + ShellEmbeddedWindow *window); + +static void +shell_gtk_embed_on_window_destroy (GtkWidget *object, + ShellGtkEmbed *embed) +{ + shell_gtk_embed_set_window (embed, NULL); +} + +static void +shell_gtk_embed_remove_window_actor (ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window_actor) + { + g_clear_signal_handler (&priv->window_actor_destroyed_handler, + priv->window_actor); + + g_object_unref (priv->window_actor); + priv->window_actor = NULL; + } + + clutter_clone_set_source (CLUTTER_CLONE (embed), NULL); +} + +static void +shell_gtk_embed_window_created_cb (MetaDisplay *display, + MetaWindow *window, + ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + Window xwindow = meta_window_get_xwindow (window); + GdkWindow *gdk_window = gtk_widget_get_window (GTK_WIDGET (priv->window)); + + if (gdk_window && xwindow == gdk_x11_window_get_xid (gdk_window)) + { + ClutterActor *window_actor = + CLUTTER_ACTOR (meta_window_get_compositor_private (window)); + GCallback remove_cb = G_CALLBACK (shell_gtk_embed_remove_window_actor); + cairo_region_t *empty_region; + + clutter_clone_set_source (CLUTTER_CLONE (embed), window_actor); + + /* We want to explicitly clear the clone source when the window + actor is destroyed because otherwise we might end up keeping + it alive after it has been disposed. Otherwise this can cause + a crash if there is a paint after mutter notices that the top + level window has been destroyed, which causes it to dispose + the window, and before the tray manager notices that the + window is gone which would otherwise reset the window and + unref the clone */ + priv->window_actor = g_object_ref (window_actor); + priv->window_actor_destroyed_handler = + g_signal_connect_swapped (window_actor, + "destroy", + remove_cb, + embed); + + /* Hide the original actor otherwise it will appear in the scene + as a normal window */ + clutter_actor_set_opacity (window_actor, 0); + + /* Also make sure it (or any of its children) doesn't block + events on wayland */ + shell_util_set_hidden_from_pick (window_actor, TRUE); + + /* Set an empty input shape on the window so that it can't get + any input. This probably isn't the ideal way to achieve this. + It would probably be better to force the window to go behind + Mutter's guard window, but this is quite difficult to do as + Mutter doesn't manage the stacking for override redirect + windows and the guard window is repeatedly lowered to the + bottom of the stack. */ + empty_region = cairo_region_create (); + gdk_window_input_shape_combine_region (gdk_window, + empty_region, + 0, 0 /* offset x/y */); + cairo_region_destroy (empty_region); + + gdk_window_lower (gdk_window); + + /* Now that we've found the window we don't need to listen for + new windows anymore */ + g_clear_signal_handler (&priv->window_created_handler, + display); + } +} + +static void +shell_gtk_embed_on_window_mapped (GtkWidget *object, + ShellGtkEmbed *embed) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + if (priv->window_created_handler == 0 && priv->window_actor == NULL) + /* Listen for new windows so we can detect when Mutter has + created a MutterWindow for this window */ + priv->window_created_handler = + g_signal_connect (display, + "window-created", + G_CALLBACK (shell_gtk_embed_window_created_cb), + embed); +} + +static void +shell_gtk_embed_set_window (ShellGtkEmbed *embed, + ShellEmbeddedWindow *window) +{ + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + MetaDisplay *display = shell_global_get_display (shell_global_get ()); + + if (priv->window) + { + g_clear_signal_handler (&priv->window_created_handler, display); + + shell_gtk_embed_remove_window_actor (embed); + + _shell_embedded_window_set_actor (priv->window, NULL); + + g_object_unref (priv->window); + + g_signal_handlers_disconnect_by_func (priv->window, + (gpointer)shell_gtk_embed_on_window_destroy, + embed); + + g_signal_handlers_disconnect_by_func (priv->window, + (gpointer)shell_gtk_embed_on_window_mapped, + embed); + } + + priv->window = window; + + if (priv->window) + { + g_object_ref (priv->window); + + _shell_embedded_window_set_actor (priv->window, embed); + + g_signal_connect (priv->window, "destroy", + G_CALLBACK (shell_gtk_embed_on_window_destroy), embed); + + g_signal_connect (priv->window, "map", + G_CALLBACK (shell_gtk_embed_on_window_mapped), embed); + } + + clutter_actor_queue_relayout (CLUTTER_ACTOR (embed)); +} + +static void +shell_gtk_embed_set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + + switch (prop_id) + { + case PROP_WINDOW: + shell_gtk_embed_set_window (embed, (ShellEmbeddedWindow *)g_value_get_object (value)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_gtk_embed_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + switch (prop_id) + { + case PROP_WINDOW: + g_value_set_object (value, priv->window); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +shell_gtk_embed_get_preferred_width (ClutterActor *actor, + float for_height, + float *min_width_p, + float *natural_width_p) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window + && gtk_widget_get_visible (GTK_WIDGET (priv->window))) + { + GtkRequisition min_req, natural_req; + gtk_widget_get_preferred_size (GTK_WIDGET (priv->window), &min_req, &natural_req); + + *min_width_p = min_req.width; + *natural_width_p = natural_req.width; + } + else + *min_width_p = *natural_width_p = 0; +} + +static void +shell_gtk_embed_get_preferred_height (ClutterActor *actor, + float for_width, + float *min_height_p, + float *natural_height_p) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + if (priv->window + && gtk_widget_get_visible (GTK_WIDGET (priv->window))) + { + GtkRequisition min_req, natural_req; + gtk_widget_get_preferred_size (GTK_WIDGET (priv->window), &min_req, &natural_req); + + *min_height_p = min_req.height; + *natural_height_p = natural_req.height; + } + else + *min_height_p = *natural_height_p = 0; +} + +static void +shell_gtk_embed_allocate (ClutterActor *actor, + const ClutterActorBox *box) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + float wx, wy; + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)-> + allocate (actor, box); + + /* Find the actor's new coordinates in terms of the stage (which is + * priv->window's parent window. + */ + clutter_actor_get_transformed_position (actor, &wx, &wy); + + _shell_embedded_window_allocate (priv->window, + (int)(0.5 + wx), (int)(0.5 + wy), + box->x2 - box->x1, + box->y2 - box->y1); +} + +static void +shell_gtk_embed_map (ClutterActor *actor) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + _shell_embedded_window_map (priv->window); + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)->map (actor); +} + +static void +shell_gtk_embed_unmap (ClutterActor *actor) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (actor); + ShellGtkEmbedPrivate *priv = shell_gtk_embed_get_instance_private (embed); + + _shell_embedded_window_unmap (priv->window); + + CLUTTER_ACTOR_CLASS (shell_gtk_embed_parent_class)->unmap (actor); +} + +static void +shell_gtk_embed_dispose (GObject *object) +{ + ShellGtkEmbed *embed = SHELL_GTK_EMBED (object); + + G_OBJECT_CLASS (shell_gtk_embed_parent_class)->dispose (object); + + shell_gtk_embed_set_window (embed, NULL); +} + +static void +shell_gtk_embed_class_init (ShellGtkEmbedClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + ClutterActorClass *actor_class = CLUTTER_ACTOR_CLASS (klass); + + object_class->get_property = shell_gtk_embed_get_property; + object_class->set_property = shell_gtk_embed_set_property; + object_class->dispose = shell_gtk_embed_dispose; + + actor_class->get_preferred_width = shell_gtk_embed_get_preferred_width; + actor_class->get_preferred_height = shell_gtk_embed_get_preferred_height; + actor_class->allocate = shell_gtk_embed_allocate; + actor_class->map = shell_gtk_embed_map; + actor_class->unmap = shell_gtk_embed_unmap; + + g_object_class_install_property (object_class, + PROP_WINDOW, + g_param_spec_object ("window", + "Window", + "ShellEmbeddedWindow to embed", + SHELL_TYPE_EMBEDDED_WINDOW, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY)); +} + +static void +shell_gtk_embed_init (ShellGtkEmbed *embed) +{ +} + +/* + * Public API + */ +ClutterActor * +shell_gtk_embed_new (ShellEmbeddedWindow *window) +{ + g_return_val_if_fail (SHELL_IS_EMBEDDED_WINDOW (window), NULL); + + return g_object_new (SHELL_TYPE_GTK_EMBED, + "window", window, + NULL); +} diff --git a/src/shell-gtk-embed.h b/src/shell-gtk-embed.h new file mode 100644 index 0000000..4cfc489 --- /dev/null +++ b/src/shell-gtk-embed.h @@ -0,0 +1,20 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_GTK_EMBED_H__ +#define __SHELL_GTK_EMBED_H__ + +#include + +#include "shell-embedded-window.h" + +#define SHELL_TYPE_GTK_EMBED (shell_gtk_embed_get_type ()) +G_DECLARE_DERIVABLE_TYPE (ShellGtkEmbed, shell_gtk_embed, + SHELL, GTK_EMBED, ClutterClone) + +struct _ShellGtkEmbedClass +{ + ClutterCloneClass parent_class; +}; + +ClutterActor *shell_gtk_embed_new (ShellEmbeddedWindow *window); + +#endif /* __SHELL_GTK_EMBED_H__ */ diff --git a/src/shell-invert-lightness-effect.c b/src/shell-invert-lightness-effect.c new file mode 100644 index 0000000..f856ede --- /dev/null +++ b/src/shell-invert-lightness-effect.c @@ -0,0 +1,152 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2010-2012 Inclusive Design Research Centre, OCAD University. + * + * 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 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 . + * + * Author: + * Joseph Scheuhammer + */ + +/** + * SECTION:shell-invert-lightness-effect + * @short_description: A colorization effect where lightness is inverted but + * color is not. + * @see_also: #ClutterEffect, #ClutterOffscreenEffect + * + * #ShellInvertLightnessEffect is a sub-class of #ClutterEffect that enhances + * the appearance of a clutter actor. Specifically it inverts the lightness + * of a #ClutterActor (e.g., darker colors become lighter, white becomes black, + * and white, black). + */ + +#define SHELL_INVERT_LIGHTNESS_EFFECT_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, ShellInvertLightnessEffectClass)) +#define SHELL_IS_INVERT_EFFECT_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT)) +#define SHELL_INVERT_LIGHTNESS_EFFECT_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFEC, ShellInvertLightnessEffectClass)) + +#include "shell-invert-lightness-effect.h" + +#include + +struct _ShellInvertLightnessEffect +{ + ClutterOffscreenEffect parent_instance; + + CoglPipeline *pipeline; +}; + +struct _ShellInvertLightnessEffectClass +{ + ClutterOffscreenEffectClass parent_class; + + CoglPipeline *base_pipeline; +}; + +/* Lightness inversion in GLSL. + */ +static const gchar *invert_lightness_source = + "cogl_texel = texture2D (cogl_sampler, cogl_tex_coord.st);\n" + "vec3 effect = vec3 (cogl_texel);\n" + "\n" + "float maxColor = max (cogl_texel.r, max (cogl_texel.g, cogl_texel.b));\n" + "float minColor = min (cogl_texel.r, min (cogl_texel.g, cogl_texel.b));\n" + "float lightness = (maxColor + minColor) / 2.0;\n" + "\n" + "float delta = (1.0 - lightness) - lightness;\n" + "effect.rgb = (effect.rgb + delta);\n" + "\n" + "cogl_texel = vec4 (effect, cogl_texel.a);\n"; + +G_DEFINE_TYPE (ShellInvertLightnessEffect, + shell_invert_lightness_effect, + CLUTTER_TYPE_OFFSCREEN_EFFECT); + +static CoglPipeline * +shell_glsl_effect_create_pipeline (ClutterOffscreenEffect *effect, + CoglTexture *texture) +{ + ShellInvertLightnessEffect *self = SHELL_INVERT_LIGHTNESS_EFFECT (effect); + + cogl_pipeline_set_layer_texture (self->pipeline, 0, texture); + + return cogl_object_ref (self->pipeline); +} + +static void +shell_invert_lightness_effect_dispose (GObject *gobject) +{ + ShellInvertLightnessEffect *self = SHELL_INVERT_LIGHTNESS_EFFECT (gobject); + + if (self->pipeline != NULL) + { + cogl_object_unref (self->pipeline); + self->pipeline = NULL; + } + + G_OBJECT_CLASS (shell_invert_lightness_effect_parent_class)->dispose (gobject); +} + +static void +shell_invert_lightness_effect_class_init (ShellInvertLightnessEffectClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + ClutterOffscreenEffectClass *offscreen_class; + + offscreen_class = CLUTTER_OFFSCREEN_EFFECT_CLASS (klass); + offscreen_class->create_pipeline = shell_glsl_effect_create_pipeline; + + gobject_class->dispose = shell_invert_lightness_effect_dispose; +} + +static void +shell_invert_lightness_effect_init (ShellInvertLightnessEffect *self) +{ + ShellInvertLightnessEffectClass *klass; + klass = SHELL_INVERT_LIGHTNESS_EFFECT_GET_CLASS (self); + + if (G_UNLIKELY (klass->base_pipeline == NULL)) + { + CoglSnippet *snippet; + CoglContext *ctx = + clutter_backend_get_cogl_context (clutter_get_default_backend ()); + + klass->base_pipeline = cogl_pipeline_new (ctx); + + snippet = cogl_snippet_new (COGL_SNIPPET_HOOK_TEXTURE_LOOKUP, + NULL, + NULL); + cogl_snippet_set_replace (snippet, invert_lightness_source); + cogl_pipeline_add_layer_snippet (klass->base_pipeline, 0, snippet); + cogl_object_unref (snippet); + + cogl_pipeline_set_layer_null_texture (klass->base_pipeline, 0); + } + + self->pipeline = cogl_pipeline_copy (klass->base_pipeline); +} + +/** + * shell_invert_lightness_effect_new: + * + * Creates a new #ShellInvertLightnessEffect to be used with + * clutter_actor_add_effect() + * + * Return value: (transfer full): the newly created + * #ShellInvertLightnessEffect or %NULL. Use g_object_unref() when done. + */ +ClutterEffect * +shell_invert_lightness_effect_new (void) +{ + return g_object_new (SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, NULL); +} diff --git a/src/shell-invert-lightness-effect.h b/src/shell-invert-lightness-effect.h new file mode 100644 index 0000000..3d7cf3a --- /dev/null +++ b/src/shell-invert-lightness-effect.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright © 2010-2012 Inclusive Design Research Centre, OCAD University. + * + * 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 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 . + * + * Author: + * Joseph Scheuhammer + */ +#ifndef __SHELL_INVERT_LIGHTNESS_EFFECT_H__ +#define __SHELL_INVERT_LIGHTNESS_EFFECT_H__ + +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_INVERT_LIGHTNESS_EFFECT (shell_invert_lightness_effect_get_type ()) +#define SHELL_INVERT_LIGHTNESS_EFFECT(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT, ShellInvertLightnessEffect)) +#define SHELL_IS_INVERT_LIGHTNESS_EFFECT(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_INVERT_LIGHTNESS_EFFECT)) + +typedef struct _ShellInvertLightnessEffect ShellInvertLightnessEffect; +typedef struct _ShellInvertLightnessEffectClass ShellInvertLightnessEffectClass; + +GType shell_invert_lightness_effect_get_type (void) G_GNUC_CONST; + +ClutterEffect *shell_invert_lightness_effect_new (void); + +G_END_DECLS + +#endif /* __SHELL_INVERT_LIGHTNESS_EFFECT_H__ */ diff --git a/src/shell-keyring-prompt.c b/src/shell-keyring-prompt.c new file mode 100644 index 0000000..bb03279 --- /dev/null +++ b/src/shell-keyring-prompt.c @@ -0,0 +1,832 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright 2012 Red Hat, Inc. + * 2012 Stef Walter + * + * 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. + * + * Author: Stef Walter + */ + +#include "config.h" + +#include "shell-keyring-prompt.h" +#include "shell-secure-text-buffer.h" + +#define GCR_API_SUBJECT_TO_CHANGE +#include + +#include + +#include + +typedef struct _ShellPasswordPromptPrivate ShellPasswordPromptPrivate; + +typedef enum +{ + PROMPTING_NONE, + PROMPTING_FOR_CONFIRM, + PROMPTING_FOR_PASSWORD +} PromptingMode; + +struct _ShellKeyringPrompt +{ + GObject parent; + + gchar *title; + gchar *message; + gchar *description; + gchar *warning; + gchar *choice_label; + gboolean choice_chosen; + gboolean password_new; + guint password_strength; + gchar *continue_label; + gchar *cancel_label; + + GTask *task; + ClutterText *password_actor; + ClutterText *confirm_actor; + PromptingMode mode; + gboolean shown; +}; + +enum { + PROP_0, + + PROP_PASSWORD_VISIBLE, + PROP_CONFIRM_VISIBLE, + PROP_WARNING_VISIBLE, + PROP_CHOICE_VISIBLE, + PROP_PASSWORD_ACTOR, + PROP_CONFIRM_ACTOR, + + N_PROPS, + + /* GcrPrompt */ + PROP_TITLE, + 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 +}; + +static GParamSpec *props[N_PROPS] = { NULL, }; + +static void shell_keyring_prompt_iface (GcrPromptInterface *iface); + +G_DEFINE_TYPE_WITH_CODE (ShellKeyringPrompt, shell_keyring_prompt, G_TYPE_OBJECT, + G_IMPLEMENT_INTERFACE (GCR_TYPE_PROMPT, shell_keyring_prompt_iface); +); + +enum { + SIGNAL_SHOW_PASSWORD, + SIGNAL_SHOW_CONFIRM, + SIGNAL_LAST +}; + +static gint signals[SIGNAL_LAST]; + +static void +shell_keyring_prompt_init (ShellKeyringPrompt *self) +{ + +} + +static gchar * +remove_mnemonics (const GValue *value) +{ + const gchar mnemonic = '_'; + gchar *stripped_label, *temp; + const gchar *label; + + g_return_val_if_fail (value != NULL, NULL); + g_return_val_if_fail (G_VALUE_HOLDS_STRING (value), NULL); + + label = g_value_get_string (value); + if (!label) + return NULL; + + /* Stripped label will have the original label length at most */ + stripped_label = temp = g_new (gchar, strlen(label) + 1); + g_assert (stripped_label != NULL); + + while (*label != '\0') + { + if (*label == mnemonic) + label++; + *(temp++) = *(label++); + } + *temp = '\0'; + + return stripped_label; +} + +static void +shell_keyring_prompt_set_property (GObject *obj, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + switch (prop_id) { + case PROP_TITLE: + g_free (self->title); + self->title = g_value_dup_string (value); + g_object_notify (obj, "title"); + break; + case PROP_MESSAGE: + g_free (self->message); + self->message = g_value_dup_string (value); + g_object_notify (obj, "message"); + break; + case PROP_DESCRIPTION: + g_free (self->description); + self->description = g_value_dup_string (value); + g_object_notify (obj, "description"); + break; + case PROP_WARNING: + g_free (self->warning); + self->warning = g_value_dup_string (value); + if (!self->warning) + self->warning = g_strdup (""); + g_object_notify (obj, "warning"); + g_object_notify_by_pspec (obj, props[PROP_WARNING_VISIBLE]); + break; + case PROP_CHOICE_LABEL: + g_free (self->choice_label); + self->choice_label = remove_mnemonics (value); + if (!self->choice_label) + self->choice_label = g_strdup (""); + g_object_notify (obj, "choice-label"); + g_object_notify_by_pspec (obj, props[PROP_CHOICE_VISIBLE]); + break; + case PROP_CHOICE_CHOSEN: + self->choice_chosen = g_value_get_boolean (value); + g_object_notify (obj, "choice-chosen"); + break; + case PROP_PASSWORD_NEW: + self->password_new = g_value_get_boolean (value); + g_object_notify (obj, "password-new"); + g_object_notify_by_pspec (obj, props[PROP_CONFIRM_VISIBLE]); + break; + case PROP_CALLER_WINDOW: + /* ignored */ + break; + case PROP_CONTINUE_LABEL: + g_free (self->continue_label); + self->continue_label = remove_mnemonics (value); + g_object_notify (obj, "continue-label"); + break; + case PROP_CANCEL_LABEL: + g_free (self->cancel_label); + self->cancel_label = remove_mnemonics (value); + g_object_notify (obj, "cancel-label"); + break; + case PROP_PASSWORD_ACTOR: + shell_keyring_prompt_set_password_actor (self, g_value_get_object (value)); + break; + case PROP_CONFIRM_ACTOR: + shell_keyring_prompt_set_confirm_actor (self, g_value_get_object (value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +shell_keyring_prompt_get_property (GObject *obj, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + switch (prop_id) { + case PROP_TITLE: + g_value_set_string (value, self->title ? self->title : ""); + break; + case PROP_MESSAGE: + g_value_set_string (value, self->message ? self->message : ""); + break; + case PROP_DESCRIPTION: + g_value_set_string (value, self->description ? self->description : ""); + break; + case PROP_WARNING: + g_value_set_string (value, self->warning ? self->warning : ""); + break; + case PROP_CHOICE_LABEL: + g_value_set_string (value, self->choice_label ? self->choice_label : ""); + break; + case PROP_CHOICE_CHOSEN: + g_value_set_boolean (value, self->choice_chosen); + break; + case PROP_PASSWORD_NEW: + g_value_set_boolean (value, self->password_new); + break; + case PROP_PASSWORD_STRENGTH: + g_value_set_int (value, self->password_strength); + break; + case PROP_CALLER_WINDOW: + g_value_set_string (value, ""); + break; + case PROP_CONTINUE_LABEL: + g_value_set_string (value, self->continue_label); + break; + case PROP_CANCEL_LABEL: + g_value_set_string (value, self->cancel_label); + break; + case PROP_PASSWORD_VISIBLE: + g_value_set_boolean (value, self->mode == PROMPTING_FOR_PASSWORD); + break; + case PROP_CONFIRM_VISIBLE: + g_value_set_boolean (value, self->password_new && + self->mode == PROMPTING_FOR_PASSWORD); + break; + case PROP_WARNING_VISIBLE: + g_value_set_boolean (value, self->warning && self->warning[0]); + break; + case PROP_CHOICE_VISIBLE: + g_value_set_boolean (value, self->choice_label && self->choice_label[0]); + break; + case PROP_PASSWORD_ACTOR: + g_value_set_object (value, shell_keyring_prompt_get_password_actor (self)); + break; + case PROP_CONFIRM_ACTOR: + g_value_set_object (value, shell_keyring_prompt_get_confirm_actor (self)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec); + break; + } +} + +static void +shell_keyring_prompt_dispose (GObject *obj) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + if (self->shown) + gcr_prompt_close (GCR_PROMPT (self)); + + if (self->task) + shell_keyring_prompt_cancel (self); + g_assert (self->task == NULL); + + shell_keyring_prompt_set_password_actor (self, NULL); + shell_keyring_prompt_set_confirm_actor (self, NULL); + + G_OBJECT_CLASS (shell_keyring_prompt_parent_class)->dispose (obj); +} + +static void +shell_keyring_prompt_finalize (GObject *obj) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (obj); + + g_free (self->title); + g_free (self->message); + g_free (self->description); + g_free (self->warning); + g_free (self->choice_label); + g_free (self->continue_label); + g_free (self->cancel_label); + + G_OBJECT_CLASS (shell_keyring_prompt_parent_class)->finalize (obj); +} + +static void +shell_keyring_prompt_class_init (ShellKeyringPromptClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_keyring_prompt_get_property; + gobject_class->set_property = shell_keyring_prompt_set_property; + gobject_class->dispose = shell_keyring_prompt_dispose; + gobject_class->finalize = shell_keyring_prompt_finalize; + + g_object_class_override_property (gobject_class, PROP_TITLE, "title"); + + g_object_class_override_property (gobject_class, PROP_MESSAGE, "message"); + + g_object_class_override_property (gobject_class, PROP_DESCRIPTION, "description"); + + g_object_class_override_property (gobject_class, PROP_WARNING, "warning"); + + g_object_class_override_property (gobject_class, PROP_PASSWORD_NEW, "password-new"); + + g_object_class_override_property (gobject_class, PROP_PASSWORD_STRENGTH, "password-strength"); + + g_object_class_override_property (gobject_class, PROP_CHOICE_LABEL, "choice-label"); + + g_object_class_override_property (gobject_class, PROP_CHOICE_CHOSEN, "choice-chosen"); + + g_object_class_override_property (gobject_class, PROP_CALLER_WINDOW, "caller-window"); + + g_object_class_override_property (gobject_class, PROP_CONTINUE_LABEL, "continue-label"); + + g_object_class_override_property (gobject_class, PROP_CANCEL_LABEL, "cancel-label"); + + /** + * ShellKeyringPrompt:password-visible: + * + * Whether the password entry is visible or not. + */ + props[PROP_PASSWORD_VISIBLE] = + g_param_spec_boolean ("password-visible", + "Password visible", + "Password field is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:confirm-visible: + * + * Whether the password confirm entry is visible or not. + */ + props[PROP_CONFIRM_VISIBLE] = + g_param_spec_boolean ("confirm-visible", + "Confirm visible", + "Confirm field is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:warning-visible: + * + * Whether the warning label is visible or not. + */ + props[PROP_WARNING_VISIBLE] = + g_param_spec_boolean ("warning-visible", + "Warning visible", + "Warning is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:choice-visible: + * + * Whether the choice check box is visible or not. + */ + props[PROP_CHOICE_VISIBLE] = + g_param_spec_boolean ("choice-visible", + "Choice visible", + "Choice is visible", + FALSE, + G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); + + /** + * ShellKeyringPrompt:password-actor: + * + * Text field for password + */ + props[PROP_PASSWORD_ACTOR] = + g_param_spec_object ("password-actor", + "Password actor", + "Text field for password", + CLUTTER_TYPE_TEXT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + /** + * ShellKeyringPrompt:confirm-actor: + * + * Text field for confirmation password + */ + props[PROP_CONFIRM_ACTOR] = + g_param_spec_object ("confirm-actor", + "Confirm actor", + "Text field for confirming password", + CLUTTER_TYPE_TEXT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (gobject_class, N_PROPS, props); + + signals[SIGNAL_SHOW_PASSWORD] = g_signal_new ("show-password", G_TYPE_FROM_CLASS (klass), + 0, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + signals[SIGNAL_SHOW_CONFIRM] = g_signal_new ("show-confirm", G_TYPE_FROM_CLASS (klass), + 0, 0, NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); +} + +static void +shell_keyring_prompt_password_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + GObject *obj; + + if (self->task != NULL) { + g_warning ("this prompt can only show one prompt at a time"); + return; + } + + self->mode = PROMPTING_FOR_PASSWORD; + self->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (self->task, shell_keyring_prompt_password_async); + + 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"); + + self->shown = TRUE; + g_signal_emit (self, signals[SIGNAL_SHOW_PASSWORD], 0); +} + +static const gchar * +shell_keyring_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, + shell_keyring_prompt_password_async), NULL); + + return g_task_propagate_pointer (G_TASK (result), error); +} + +static void +shell_keyring_prompt_confirm_async (GcrPrompt *prompt, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + GObject *obj; + + if (self->task != NULL) { + g_warning ("this prompt is already prompting"); + return; + } + + self->mode = PROMPTING_FOR_CONFIRM; + self->task = g_task_new (self, NULL, callback, user_data); + g_task_set_source_tag (self->task, shell_keyring_prompt_confirm_async); + + 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"); + + self->shown = TRUE; + g_signal_emit (self, signals[SIGNAL_SHOW_CONFIRM], 0); +} + +static GcrPromptReply +shell_keyring_prompt_confirm_finish (GcrPrompt *prompt, + GAsyncResult *result, + GError **error) +{ + GTask *task = G_TASK (result); + gssize res; + + 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, + shell_keyring_prompt_confirm_async), GCR_PROMPT_REPLY_CANCEL); + + res = g_task_propagate_int (task, error); + return res == -1 ? GCR_PROMPT_REPLY_CANCEL : (GcrPromptReply)res; +} + +static void +shell_keyring_prompt_close (GcrPrompt *prompt) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (prompt); + + /* + * We expect keyring.js to connect to this signal and do the + * actual work of closing the prompt. + */ + + self->shown = FALSE; +} + +static void +shell_keyring_prompt_iface (GcrPromptInterface *iface) +{ + iface->prompt_password_async = shell_keyring_prompt_password_async; + iface->prompt_password_finish = shell_keyring_prompt_password_finish; + iface->prompt_confirm_async = shell_keyring_prompt_confirm_async; + iface->prompt_confirm_finish = shell_keyring_prompt_confirm_finish; + iface->prompt_close = shell_keyring_prompt_close; +} + +/** + * shell_keyring_prompt_new: + * + * Create new internal prompt base + * + * Returns: (transfer full): new internal prompt + */ +ShellKeyringPrompt * +shell_keyring_prompt_new (void) +{ + return g_object_new (SHELL_TYPE_KEYRING_PROMPT, NULL); +} + +/** + * shell_keyring_prompt_get_password_actor: + * @self: the internal prompt + * + * Get the prompt password text actor + * + * Returns: (transfer none) (nullable): the password actor + */ +ClutterText * +shell_keyring_prompt_get_password_actor (ShellKeyringPrompt *self) +{ + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), NULL); + return self->password_actor; +} + +/** + * shell_keyring_prompt_get_confirm_actor: + * @self: the internal prompt + * + * Get the prompt password text actor + * + * Returns: (transfer none) (nullable): the password actor + */ +ClutterText * +shell_keyring_prompt_get_confirm_actor (ShellKeyringPrompt *self) +{ + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), NULL); + return self->confirm_actor; +} + +static guint +calculate_password_strength (const gchar *password) +{ + int upper, lower, digit, misc; + gdouble pwstrength; + int length, i; + + /* + * 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); + + /* Always return 0 for empty passwords */ + if (length == 0) + return 0; + + upper = 0; + lower = 0; + digit = 0; + misc = 0; + + for (i = 0; i < length ; i++) + { + if (g_ascii_isdigit (password[i])) + digit++; + else if (g_ascii_islower (password[i])) + lower++; + else if (g_ascii_isupper (password[i])) + upper++; + else + misc++; + } + + if (length > 5) + length = 5; + if (digit > 3) + digit = 3; + if (upper > 3) + upper = 3; + if (misc > 3) + misc = 3; + + pwstrength = ((length * 1) - 2) + + (digit * 1) + + (misc * 1.5) + + (upper * 1); + + /* Always return 1+ for non-empty passwords */ + if (pwstrength < 1.0) + pwstrength = 1.0; + if (pwstrength > 10.0) + pwstrength = 10.0; + + return (guint)pwstrength; +} + +static void +on_password_changed (ClutterText *text, + gpointer user_data) +{ + ShellKeyringPrompt *self = SHELL_KEYRING_PROMPT (user_data); + const gchar *password; + + password = clutter_text_get_text (self->password_actor); + + self->password_strength = calculate_password_strength (password); + g_object_notify (G_OBJECT (self), "password-strength"); +} + +/** + * shell_keyring_prompt_set_password_actor: + * @self: the internal prompt + * @password_actor: (nullable): the password actor + * + * Set the prompt password text actor + */ +void +shell_keyring_prompt_set_password_actor (ShellKeyringPrompt *self, + ClutterText *password_actor) +{ + ClutterTextBuffer *buffer; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + g_return_if_fail (password_actor == NULL || CLUTTER_IS_TEXT (password_actor)); + + if (self->password_actor == password_actor) + return; + + if (password_actor) + { + buffer = shell_secure_text_buffer_new (); + clutter_text_set_buffer (password_actor, buffer); + g_object_unref (buffer); + + g_signal_connect (password_actor, "text-changed", G_CALLBACK (on_password_changed), self); + g_object_ref (password_actor); + } + if (self->password_actor) + { + g_signal_handlers_disconnect_by_func (self->password_actor, on_password_changed, self); + g_object_unref (self->password_actor); + } + + self->password_actor = password_actor; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PASSWORD_ACTOR]); +} + +/** + * shell_keyring_prompt_set_confirm_actor: + * @self: the internal prompt + * @confirm_actor: (nullable): the confirm password actor + * + * Set the prompt password confirmation text actor + */ +void +shell_keyring_prompt_set_confirm_actor (ShellKeyringPrompt *self, + ClutterText *confirm_actor) +{ + ClutterTextBuffer *buffer; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + g_return_if_fail (confirm_actor == NULL || CLUTTER_IS_TEXT (confirm_actor)); + + if (self->confirm_actor == confirm_actor) + return; + + if (confirm_actor) + { + buffer = shell_secure_text_buffer_new (); + clutter_text_set_buffer (confirm_actor, buffer); + g_object_unref (buffer); + + g_object_ref (confirm_actor); + } + if (self->confirm_actor) + g_object_unref (self->confirm_actor); + self->confirm_actor = confirm_actor; + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CONFIRM_ACTOR]); +} + +/** + * shell_keyring_prompt_complete: + * @self: the internal prompt + * + * Called by the implementation when the prompt completes. There are various + * checks done. %TRUE is returned if the prompt actually should complete. + * + * Returns: whether the prompt completed + */ +gboolean +shell_keyring_prompt_complete (ShellKeyringPrompt *self) +{ + GTask *res; + PromptingMode mode; + const gchar *password; + const gchar *confirm; + const gchar *env; + + g_return_val_if_fail (SHELL_IS_KEYRING_PROMPT (self), FALSE); + g_return_val_if_fail (self->mode != PROMPTING_NONE, FALSE); + g_return_val_if_fail (self->task != NULL, FALSE); + + password = clutter_text_get_text (self->password_actor); + + if (self->mode == PROMPTING_FOR_PASSWORD) + { + /* Is it a new password? */ + if (self->password_new) + { + confirm = clutter_text_get_text (self->confirm_actor); + + /* 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; + } + } + + self->password_strength = calculate_password_strength (password); + g_object_notify (G_OBJECT (self), "password-strength"); + } + + res = self->task; + mode = self->mode; + self->task = NULL; + self->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); + + return TRUE; +} + +/** + * shell_keyring_prompt_cancel: + * @self: the internal prompt + * + * Called by implementation when the prompt is cancelled. + */ +void +shell_keyring_prompt_cancel (ShellKeyringPrompt *self) +{ + GTask *res; + PromptingMode mode; + + g_return_if_fail (SHELL_IS_KEYRING_PROMPT (self)); + + /* + * If cancelled while not prompting, we should just close the prompt, + * the user wants it to go away. + */ + if (self->mode == PROMPTING_NONE) + { + if (self->shown) + gcr_prompt_close (GCR_PROMPT (self)); + return; + } + + g_return_if_fail (self->task != NULL); + + res = self->task; + mode = self->mode; + self->task = NULL; + self->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); +} diff --git a/src/shell-keyring-prompt.h b/src/shell-keyring-prompt.h new file mode 100644 index 0000000..fcacf4c --- /dev/null +++ b/src/shell-keyring-prompt.h @@ -0,0 +1,57 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* shell-keyring-prompt.c - prompt handler for gnome-keyring-daemon + + Copyright (C) 2011 Stefan Walter + + 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., 675 Mass Ave, Cambridge, MA 02139, USA. + + Author: Stef Walter +*/ + +#ifndef __SHELL_KEYRING_PROMPT_H__ +#define __SHELL_KEYRING_PROMPT_H__ + +#include +#include + +#include + +G_BEGIN_DECLS + +typedef struct _ShellKeyringPrompt ShellKeyringPrompt; + +#define SHELL_TYPE_KEYRING_PROMPT (shell_keyring_prompt_get_type ()) +G_DECLARE_FINAL_TYPE (ShellKeyringPrompt, shell_keyring_prompt, + SHELL, KEYRING_PROMPT, GObject) + +ShellKeyringPrompt * shell_keyring_prompt_new (void); + +ClutterText * shell_keyring_prompt_get_password_actor (ShellKeyringPrompt *self); + +void shell_keyring_prompt_set_password_actor (ShellKeyringPrompt *self, + ClutterText *password_actor); + +ClutterText * shell_keyring_prompt_get_confirm_actor (ShellKeyringPrompt *self); + +void shell_keyring_prompt_set_confirm_actor (ShellKeyringPrompt *self, + ClutterText *confirm_actor); + +gboolean shell_keyring_prompt_complete (ShellKeyringPrompt *self); + +void shell_keyring_prompt_cancel (ShellKeyringPrompt *self); + +G_END_DECLS + +#endif /* __SHELL_KEYRING_PROMPT_H__ */ diff --git a/src/shell-mount-operation.c b/src/shell-mount-operation.c new file mode 100644 index 0000000..8d43368 --- /dev/null +++ b/src/shell-mount-operation.c @@ -0,0 +1,189 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * Author: Cosimo Cecchi + * + */ + +#include "shell-mount-operation.h" + +/* This is a dummy class; we would like to be able to subclass the + * object from JS but we can't yet; the default GMountOperation impl + * automatically calls g_mount_operation_reply(UNHANDLED) after an idle, + * in interactive methods. We want to handle the reply ourselves + * instead, so we just override the default methods with empty ones, + * except for ask-password, as we don't want to handle that. + * + * Also, we need to workaround the fact that gjs doesn't support type + * annotations for signals yet (so we can't effectively forward e.g. + * the GPid array to JS). + * See https://bugzilla.gnome.org/show_bug.cgi?id=645978 + */ + +enum { + SHOW_PROCESSES_2, + NUM_SIGNALS +}; + +static guint signals[NUM_SIGNALS] = { 0, }; + +typedef struct _ShellMountOperationPrivate ShellMountOperationPrivate; + +struct _ShellMountOperation +{ + GMountOperation parent_instance; + + ShellMountOperationPrivate *priv; +}; + +struct _ShellMountOperationPrivate { + GArray *pids; + gchar **choices; + gchar *message; +}; + +G_DEFINE_TYPE_WITH_PRIVATE (ShellMountOperation, shell_mount_operation, G_TYPE_MOUNT_OPERATION); + +static void +shell_mount_operation_init (ShellMountOperation *self) +{ + self->priv = shell_mount_operation_get_instance_private (self); +} + +static void +shell_mount_operation_ask_password (GMountOperation *op, + const char *message, + const char *default_user, + const char *default_domain, + GAskPasswordFlags flags) +{ + /* do nothing */ +} + +static void +shell_mount_operation_ask_question (GMountOperation *op, + const char *message, + const char *choices[]) +{ + /* do nothing */ +} + +static void +shell_mount_operation_show_processes (GMountOperation *operation, + const gchar *message, + GArray *processes, + const gchar *choices[]) +{ + ShellMountOperation *self = SHELL_MOUNT_OPERATION (operation); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + g_free (self->priv->message); + g_strfreev (self->priv->choices); + + /* save the parameters */ + self->priv->pids = g_array_ref (processes); + self->priv->choices = g_strdupv ((gchar **) choices); + self->priv->message = g_strdup (message); + + g_signal_emit (self, signals[SHOW_PROCESSES_2], 0); +} + +static void +shell_mount_operation_finalize (GObject *obj) +{ + ShellMountOperation *self = SHELL_MOUNT_OPERATION (obj); + + g_strfreev (self->priv->choices); + g_free (self->priv->message); + + if (self->priv->pids != NULL) + { + g_array_unref (self->priv->pids); + self->priv->pids = NULL; + } + + G_OBJECT_CLASS (shell_mount_operation_parent_class)->finalize (obj); +} + +static void +shell_mount_operation_class_init (ShellMountOperationClass *klass) +{ + GMountOperationClass *mclass; + GObjectClass *oclass; + + mclass = G_MOUNT_OPERATION_CLASS (klass); + mclass->show_processes = shell_mount_operation_show_processes; + mclass->ask_question = shell_mount_operation_ask_question; + mclass->ask_password = shell_mount_operation_ask_password; + + oclass = G_OBJECT_CLASS (klass); + oclass->finalize = shell_mount_operation_finalize; + + signals[SHOW_PROCESSES_2] = + g_signal_new ("show-processes-2", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} + +GMountOperation * +shell_mount_operation_new (void) +{ + return g_object_new (SHELL_TYPE_MOUNT_OPERATION, NULL); +} + +/** + * shell_mount_operation_get_show_processes_pids: + * @self: a #ShellMountOperation + * + * Returns: (transfer full) (element-type GPid): a #GArray + */ +GArray * +shell_mount_operation_get_show_processes_pids (ShellMountOperation *self) +{ + return g_array_ref (self->priv->pids); +} + +/** + * shell_mount_operation_get_show_processes_choices: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar ** +shell_mount_operation_get_show_processes_choices (ShellMountOperation *self) +{ + return g_strdupv (self->priv->choices); +} + +/** + * shell_mount_operation_get_show_processes_message: + * @self: a #ShellMountOperation + * + * Returns: (transfer full): + */ +gchar * +shell_mount_operation_get_show_processes_message (ShellMountOperation *self) +{ + return g_strdup (self->priv->message); +} diff --git a/src/shell-mount-operation.h b/src/shell-mount-operation.h new file mode 100644 index 0000000..c4019a5 --- /dev/null +++ b/src/shell-mount-operation.h @@ -0,0 +1,41 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +/* + * Copyright (C) 2011 Red Hat, Inc. + * + * 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, see . + * + * Author: Cosimo Cecchi + * + */ + +#ifndef __SHELL_MOUNT_OPERATION_H__ +#define __SHELL_MOUNT_OPERATION_H__ + +#include + +G_BEGIN_DECLS + +#define SHELL_TYPE_MOUNT_OPERATION (shell_mount_operation_get_type ()) +G_DECLARE_FINAL_TYPE (ShellMountOperation, shell_mount_operation, + SHELL, MOUNT_OPERATION, GMountOperation) + +GMountOperation *shell_mount_operation_new (void); + +GArray * shell_mount_operation_get_show_processes_pids (ShellMountOperation *self); +gchar ** shell_mount_operation_get_show_processes_choices (ShellMountOperation *self); +gchar * shell_mount_operation_get_show_processes_message (ShellMountOperation *self); + +G_END_DECLS + +#endif /* __SHELL_MOUNT_OPERATION_H__ */ diff --git a/src/shell-network-agent.c b/src/shell-network-agent.c new file mode 100644 index 0000000..474394f --- /dev/null +++ b/src/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 "config.h" +#include + +#include + +#include "shell-network-agent.h" + +enum { + SIGNAL_NEW_REQUEST, + SIGNAL_CANCEL_REQUEST, + SIGNAL_LAST +}; + +static gint signals[SIGNAL_LAST]; + +typedef struct { + GCancellable * cancellable; + ShellNetworkAgent *self; + + gchar *request_id; + NMConnection *connection; + gchar *setting_name; + gchar **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 = { + "org.freedesktop.NetworkManager.Connection", + SECRET_SCHEMA_DONT_MATCH_NAME, + { + { 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_free (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 gchar *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 gchar *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; + g_autoptr (GVariant) setting = NULL; + + 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 gchar *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_ref_sink (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 gchar *connection_path, + const gchar *setting_name, + const gchar **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_new0 (ShellAgentRequest, 1); + 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 ((gchar **)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, + 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_dict_insert (request->entries, setting_key, "s", setting_value); +} + +void +shell_network_agent_respond (ShellNetworkAgent *self, + gchar *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 gchar *connection_path, + const gchar *setting_name) +{ + ShellNetworkAgent *self = SHELL_NETWORK_AGENT (agent); + ShellNetworkAgentPrivate *priv = self->priv; + gchar *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 gchar *connection_uuid, + const gchar *connection_id, + const gchar *setting_name, + const gchar *setting_key, + gchar **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 */ + gint 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_free (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 gchar *key, + const gchar *secret, + const gchar *display_name) +{ + GHashTable *attrs; + gchar *alt_display_name = NULL; + const gchar *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 gchar *key, + const gchar *secret, + gpointer user_data) +{ + KeyringRequest *r = user_data; + NMSetting *setting; + const gchar *service_name, *id; + gchar *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 gchar *key, + const GValue *value, + GParamFlags flags, + gpointer user_data) +{ + KeyringRequest *r = user_data; + const gchar *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 gchar *connection_path, + NMSecretAgentOldSaveSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + + r = g_new (KeyringRequest, 1); + 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 gchar *connection_path, + NMSecretAgentOldDeleteSecretsFunc callback, + gpointer callback_data) +{ + KeyringRequest *r; + NMSettingConnection *s_con; + const gchar *uuid; + + r = g_new (KeyringRequest, 1); + 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/shell-network-agent.h b/src/shell-network-agent.h new file mode 100644 index 0000000..0ffde02 --- /dev/null +++ b/src/shell-network-agent.h @@ -0,0 +1,73 @@ +/* -*- 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 + +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, + gchar *request_id, + gchar *setting_key, + gchar *setting_value); +void shell_network_agent_respond (ShellNetworkAgent *self, + gchar *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/shell-perf-helper.c b/src/shell-perf-helper.c new file mode 100644 index 0000000..a50376e --- /dev/null +++ b/src/shell-perf-helper.c @@ -0,0 +1,376 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +/* gnome-shell-perf-helper: a program to create windows for performance tests + * + * Running performance tests with whatever windows a user has open results + * in unreliable results, so instead we hide all other windows and talk + * to this program over D-Bus to create just the windows we want. + */ + +#include "config.h" + +#include + +#include + +#define BUS_NAME "org.gnome.Shell.PerfHelper" + +static void destroy_windows (void); +static void finish_wait_windows (void); +static void check_finish_wait_windows (void); + +static const gchar introspection_xml[] = + "" + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + " " + ""; + +typedef struct { + GtkWidget *window; + int width; + int height; + + guint alpha : 1; + guint maximized : 1; + guint redraws : 1; + guint mapped : 1; + guint exposed : 1; + guint pending : 1; + + gint64 start_time; + gint64 time; +} WindowInfo; + +static int opt_idle_timeout = 30; + +static GOptionEntry opt_entries[] = + { + { "idle-timeout", 'r', 0, G_OPTION_ARG_INT, &opt_idle_timeout, "Exit after N seconds", "N" }, + { NULL } + }; + +static guint timeout_id; +static GList *our_windows; +static GList *wait_windows_invocations; + +static gboolean +on_timeout (gpointer data) +{ + timeout_id = 0; + + destroy_windows (); + gtk_main_quit (); + + return FALSE; +} + +static void +establish_timeout (void) +{ + g_clear_handle_id (&timeout_id, g_source_remove); + + timeout_id = g_timeout_add (opt_idle_timeout * 1000, on_timeout, NULL); + g_source_set_name_by_id (timeout_id, "[gnome-shell] on_timeout"); +} + +static void +destroy_windows (void) +{ + GList *l; + + for (l = our_windows; l; l = l->next) + { + WindowInfo *info = l->data; + gtk_widget_destroy (info->window); + g_free (info); + } + + g_list_free (our_windows); + our_windows = NULL; + + check_finish_wait_windows (); +} + +static gboolean +on_window_map_event (GtkWidget *window, + GdkEventAny *event, + WindowInfo *info) +{ + info->mapped = TRUE; + + return FALSE; +} + +static gboolean +on_child_draw (GtkWidget *window, + cairo_t *cr, + WindowInfo *info) +{ + cairo_rectangle_int_t allocation; + double x_offset, y_offset; + + gtk_widget_get_allocation (window, &allocation); + + /* We draw an arbitrary pattern of red lines near the border of the + * window to make it more clear than empty windows if something + * is drastrically wrong. + */ + + cairo_save (cr); + cairo_set_operator (cr, CAIRO_OPERATOR_SOURCE); + + if (info->alpha) + cairo_set_source_rgba (cr, 1, 1, 1, 0.5); + else + cairo_set_source_rgb (cr, 1, 1, 1); + + cairo_paint (cr); + cairo_restore (cr); + + if (info->redraws) + { + double position = (info->time - info->start_time) / 1000000.; + x_offset = 20 * cos (2 * M_PI * position); + y_offset = 20 * sin (2 * M_PI * position); + } + else + { + x_offset = y_offset = 0; + } + + cairo_set_source_rgb (cr, 1, 0, 0); + cairo_set_line_width (cr, 10); + cairo_move_to (cr, 0, 40 + y_offset); + cairo_line_to (cr, allocation.width, 40 + y_offset); + cairo_move_to (cr, 0, allocation.height - 40 + y_offset); + cairo_line_to (cr, allocation.width, allocation.height - 40 + y_offset); + cairo_move_to (cr, 40 + x_offset, 0); + cairo_line_to (cr, 40 + x_offset, allocation.height); + cairo_move_to (cr, allocation.width - 40 + x_offset, 0); + cairo_line_to (cr, allocation.width - 40 + x_offset, allocation.height); + cairo_stroke (cr); + + info->exposed = TRUE; + + if (info->exposed && info->mapped && info->pending) + { + info->pending = FALSE; + check_finish_wait_windows (); + } + + return FALSE; +} + +static gboolean +tick_callback (GtkWidget *widget, + GdkFrameClock *frame_clock, + gpointer user_data) +{ + WindowInfo *info = user_data; + + if (info->start_time < 0) + info->start_time = info->time = gdk_frame_clock_get_frame_time (frame_clock); + else + info->time = gdk_frame_clock_get_frame_time (frame_clock); + + gtk_widget_queue_draw (widget); + + return TRUE; +} + +static void +create_window (int width, + int height, + gboolean alpha, + gboolean maximized, + gboolean redraws) +{ + WindowInfo *info; + GtkWidget *child; + + info = g_new0 (WindowInfo, 1); + info->width = width; + info->height = height; + info->alpha = alpha; + info->maximized = maximized; + info->redraws = redraws; + info->window = gtk_window_new (GTK_WINDOW_TOPLEVEL); + if (alpha) + gtk_widget_set_visual (info->window, gdk_screen_get_rgba_visual (gdk_screen_get_default ())); + if (maximized) + gtk_window_maximize (GTK_WINDOW (info->window)); + info->pending = TRUE; + info->start_time = -1; + + child = g_object_new (GTK_TYPE_BOX, "visible", TRUE, "app-paintable", TRUE, NULL); + gtk_container_add (GTK_CONTAINER (info->window), child); + + gtk_widget_set_size_request (info->window, width, height); + gtk_widget_set_app_paintable (info->window, TRUE); + g_signal_connect (info->window, "map-event", G_CALLBACK (on_window_map_event), info); + g_signal_connect (child, "draw", G_CALLBACK (on_child_draw), info); + gtk_widget_show (info->window); + + if (info->redraws) + gtk_widget_add_tick_callback (info->window, tick_callback, + info, NULL); + + our_windows = g_list_prepend (our_windows, info); +} + +static void +finish_wait_windows (void) +{ + GList *l; + + for (l = wait_windows_invocations; l; l = l->next) + g_dbus_method_invocation_return_value (l->data, NULL); + + g_list_free (wait_windows_invocations); + wait_windows_invocations = NULL; +} + +static void +check_finish_wait_windows (void) +{ + GList *l; + gboolean have_pending = FALSE; + + for (l = our_windows; l; l = l->next) + { + WindowInfo *info = l->data; + if (info->pending) + have_pending = TRUE; + } + + if (!have_pending) + finish_wait_windows (); +} + +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) +{ + /* Push off the idle timeout */ + establish_timeout (); + + if (g_strcmp0 (method_name, "Exit") == 0) + { + destroy_windows (); + + g_dbus_method_invocation_return_value (invocation, NULL); + g_dbus_connection_flush_sync (connection, NULL, NULL); + + gtk_main_quit (); + } + else if (g_strcmp0 (method_name, "CreateWindow") == 0) + { + int width, height; + gboolean alpha, maximized, redraws; + + g_variant_get (parameters, "(iibbb)", &width, &height, &alpha, &maximized, &redraws); + + create_window (width, height, alpha, maximized, redraws); + g_dbus_method_invocation_return_value (invocation, NULL); + } + else if (g_strcmp0 (method_name, "WaitWindows") == 0) + { + wait_windows_invocations = g_list_prepend (wait_windows_invocations, invocation); + check_finish_wait_windows (); + } + else if (g_strcmp0 (method_name, "DestroyWindows") == 0) + { + destroy_windows (); + g_dbus_method_invocation_return_value (invocation, NULL); + } +} + +static const GDBusInterfaceVTable interface_vtable = +{ + handle_method_call, + NULL, + NULL +}; + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + GDBusNodeInfo *introspection_data = g_dbus_node_info_new_for_xml (introspection_xml, NULL); + + g_dbus_connection_register_object (connection, + "/org/gnome/Shell/PerfHelper", + introspection_data->interfaces[0], + &interface_vtable, + NULL, /* user_data */ + NULL, /* user_data_free_func */ + NULL); /* GError** */ +} + +static void +on_name_acquired (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ +} + +static void +on_name_lost (GDBusConnection *connection, + const gchar *name, + gpointer user_data) +{ + destroy_windows (); + gtk_main_quit (); +} + +int +main (int argc, char **argv) +{ + GOptionContext *context; + GError *error = NULL; + + /* Since we depend on this, avoid the possibility of lt-gnome-shell-perf-helper */ + g_set_prgname ("gnome-shell-perf-helper"); + + context = g_option_context_new (" - server to create windows for performance testing"); + g_option_context_add_main_entries (context, opt_entries, NULL); + g_option_context_add_group (context, gtk_get_option_group (TRUE)); + if (!g_option_context_parse (context, &argc, &argv, &error)) + { + g_print ("option parsing failed: %s\n", error->message); + return 1; + } + + 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, + NULL, + NULL); + + establish_timeout (); + + gtk_main (); + + return 0; +} diff --git a/src/shell-perf-log.c b/src/shell-perf-log.c new file mode 100644 index 0000000..3bd5228 --- /dev/null +++ b/src/shell-perf-log.c @@ -0,0 +1,959 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "config.h" + +#include + +#include "shell-perf-log.h" + +typedef struct _ShellPerfEvent ShellPerfEvent; +typedef struct _ShellPerfStatistic ShellPerfStatistic; +typedef struct _ShellPerfStatisticsClosure ShellPerfStatisticsClosure; +typedef union _ShellPerfStatisticValue ShellPerfStatisticValue; +typedef struct _ShellPerfBlock ShellPerfBlock; + +/** + * SECTION:shell-perf-log + * @short_description: Event recorder for performance measurement + * + * ShellPerfLog provides a way for different parts of the code to + * record information for subsequent analysis and interactive + * exploration. Events exist of a timestamp, an event ID, and + * arguments to the event. + * + * Emphasis is placed on storing recorded events in a compact + * fashion so log recording disturbs the execution of the program + * as little as possible, however events should not be recorded + * at too fine a granularity - an event that is recorded once + * per frame or once per user action is appropriate, an event that + * occurs many times per frame is not. + * + * Arguments are identified by a D-Bus style signature; at the moment + * only a limited number of event signatures are supported to + * simplify the code. + */ +struct _ShellPerfLog +{ + GObject parent; + + GPtrArray *events; + GHashTable *events_by_name; + GPtrArray *statistics; + GHashTable *statistics_by_name; + + GPtrArray *statistics_closures; + + GQueue *blocks; + + gint64 start_time; + gint64 last_time; + + guint statistics_timeout_id; + + guint enabled : 1; +}; + +struct _ShellPerfEvent +{ + guint16 id; + char *name; + char *description; + char *signature; +}; + +union _ShellPerfStatisticValue +{ + int i; + gint64 x; +}; + +struct _ShellPerfStatistic +{ + ShellPerfEvent *event; + + ShellPerfStatisticValue current_value; + ShellPerfStatisticValue last_value; + + guint initialized : 1; + guint recorded : 1; +}; + +struct _ShellPerfStatisticsClosure +{ + ShellPerfStatisticsCallback callback; + gpointer user_data; + GDestroyNotify notify; +}; + +/* The events in the log are stored in a linked list of fixed size + * blocks. + * + * Note that the power-of-two nature of BLOCK_SIZE here is superficial + * since the allocated block has the 'bytes' field and malloc + * overhead. The current value is well below the size that will + * typically be independently mmapped by the malloc implementation so + * it doesn't matter. If we switched to mmapping blocks manually + * (perhaps to avoid polluting malloc statistics), we'd want to use a + * different value of BLOCK_SIZE. + */ +#define BLOCK_SIZE 8192 + +struct _ShellPerfBlock +{ + guint32 bytes; + guchar buffer[BLOCK_SIZE]; +}; + +/* Number of milliseconds between periodic statistics collection when + * events are enabled. Statistics collection can also be explicitly + * triggered. + */ +#define STATISTIC_COLLECTION_INTERVAL_MS 5000 + +/* Builtin events */ +enum { + EVENT_SET_TIME, + EVENT_STATISTICS_COLLECTED +}; + +G_DEFINE_TYPE(ShellPerfLog, shell_perf_log, G_TYPE_OBJECT); + +static gint64 +get_time (void) +{ + return g_get_monotonic_time (); +} + +static void +shell_perf_log_init (ShellPerfLog *perf_log) +{ + perf_log->events = g_ptr_array_new (); + perf_log->events_by_name = g_hash_table_new (g_str_hash, g_str_equal); + perf_log->statistics = g_ptr_array_new (); + perf_log->statistics_by_name = g_hash_table_new (g_str_hash, g_str_equal); + perf_log->statistics_closures = g_ptr_array_new (); + perf_log->blocks = g_queue_new (); + + /* This event is used when timestamp deltas are greater than + * fits in a gint32. 0xffffffff microseconds is about 70 minutes, so this + * is not going to happen in normal usage. It might happen if performance + * logging is enabled some time after starting the shell */ + shell_perf_log_define_event (perf_log, "perf.setTime", "", "x"); + g_assert (perf_log->events->len == EVENT_SET_TIME + 1); + + /* The purpose of this event is to allow us to optimize out storing + * statistics that haven't changed. We want to mark every time we + * collect statistics even if we don't record any individual + * statistics so that we can distinguish sudden changes from gradual changes. + * + * The argument is the number of microseconds that statistics collection + * took; we record that since statistics collection could start taking + * significant time if we do things like grub around in /proc/ + */ + shell_perf_log_define_event (perf_log, "perf.statisticsCollected", + "Finished collecting statistics", + "x"); + g_assert (perf_log->events->len == EVENT_STATISTICS_COLLECTED + 1); + + perf_log->start_time = perf_log->last_time = get_time(); +} + +static void +shell_perf_log_class_init (ShellPerfLogClass *class) +{ +} + +/** + * shell_perf_log_get_default: + * + * Gets the global singleton performance log. This is initially disabled + * and must be explicitly enabled with shell_perf_log_set_enabled(). + * + * Return value: (transfer none): the global singleton performance log + */ +ShellPerfLog * +shell_perf_log_get_default (void) +{ + static ShellPerfLog *perf_log; + + if (perf_log == NULL) + perf_log = g_object_new (SHELL_TYPE_PERF_LOG, NULL); + + return perf_log; +} + +static gboolean +statistics_timeout (gpointer data) +{ + ShellPerfLog *perf_log = data; + + shell_perf_log_collect_statistics (perf_log); + + return TRUE; +} + +/** + * shell_perf_log_set_enabled: + * @perf_log: a #ShellPerfLog + * @enabled: whether to record events + * + * Sets whether events are currently being recorded. + */ +void +shell_perf_log_set_enabled (ShellPerfLog *perf_log, + gboolean enabled) +{ + enabled = enabled != FALSE; + + if (enabled != perf_log->enabled) + { + perf_log->enabled = enabled; + + if (enabled) + { + perf_log->statistics_timeout_id = g_timeout_add (STATISTIC_COLLECTION_INTERVAL_MS, + statistics_timeout, + perf_log); + g_source_set_name_by_id (perf_log->statistics_timeout_id, "[gnome-shell] statistics_timeout"); + } + else + { + g_clear_handle_id (&perf_log->statistics_timeout_id, g_source_remove); + } + } +} + +static ShellPerfEvent * +define_event (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + ShellPerfEvent *event; + + if (strcmp (signature, "") != 0 && + strcmp (signature, "s") != 0 && + strcmp (signature, "i") != 0 && + strcmp (signature, "x") != 0) + { + g_warning ("Only supported event signatures are '', 's', 'i', and 'x'\n"); + return NULL; + } + + if (perf_log->events->len == 65536) + { + g_warning ("Maximum number of events defined\n"); + return NULL; + } + + /* We could do stricter validation, but this will break our JSON dumps */ + if (strchr (name, '"') != NULL) + { + g_warning ("Event names can't include '\"'"); + return NULL; + } + + if (g_hash_table_lookup (perf_log->events_by_name, name) != NULL) + { + g_warning ("Duplicate event event for '%s'\n", name); + return NULL; + } + + event = g_new (ShellPerfEvent, 1); + + event->id = perf_log->events->len; + event->name = g_strdup (name); + event->signature = g_strdup (signature); + event->description = g_strdup (description); + + g_ptr_array_add (perf_log->events, event); + g_hash_table_insert (perf_log->events_by_name, event->name, event); + + return event; +} + +/** + * shell_perf_log_define_event: + * @perf_log: a #ShellPerfLog + * @name: name of the event. This should of the form + * '., for example + * 'clutter.stagePaintDone'. + * @description: human readable description of the event. + * @signature: signature defining the arguments that event takes. + * This is a string of type characters, using the same characters + * as D-Bus or GVariant. Only a very limited number of signatures + * are supported: , '', 's', 'i', and 'x'. This mean respectively: + * no arguments, one string, one 32-bit integer, and one 64-bit + * integer. + * + * Defines a performance event for later recording. + */ +void +shell_perf_log_define_event (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + define_event (perf_log, name, description, signature); +} + +static ShellPerfEvent * +lookup_event (ShellPerfLog *perf_log, + const char *name, + const char *signature) +{ + ShellPerfEvent *event = g_hash_table_lookup (perf_log->events_by_name, name); + + if (G_UNLIKELY (event == NULL)) + { + g_warning ("Discarding unknown event '%s'\n", name); + return NULL; + } + + if (G_UNLIKELY (strcmp (event->signature, signature) != 0)) + { + g_warning ("Event '%s'; defined with signature '%s', used with '%s'\n", + name, event->signature, signature); + return NULL; + } + + return event; +} + +static void +record_event (ShellPerfLog *perf_log, + gint64 event_time, + ShellPerfEvent *event, + const guchar *bytes, + size_t bytes_len) +{ + ShellPerfBlock *block; + size_t total_bytes; + guint32 time_delta; + guint32 pos; + + if (!perf_log->enabled) + return; + + total_bytes = sizeof (gint32) + sizeof (gint16) + bytes_len; + if (G_UNLIKELY (bytes_len > BLOCK_SIZE || total_bytes > BLOCK_SIZE)) + { + g_warning ("Discarding oversize event '%s'\n", event->name); + return; + } + + if (event_time > perf_log->last_time + G_GINT64_CONSTANT(0xffffffff)) + { + perf_log->last_time = event_time; + record_event (perf_log, event_time, + lookup_event (perf_log, "perf.setTime", "x"), + (const guchar *)&event_time, sizeof(gint64)); + time_delta = 0; + } + else if (event_time < perf_log->last_time) + time_delta = 0; + else + time_delta = (guint32)(event_time - perf_log->last_time); + + perf_log->last_time = event_time; + + if (perf_log->blocks->tail == NULL || + total_bytes + ((ShellPerfBlock *)perf_log->blocks->tail->data)->bytes > BLOCK_SIZE) + { + block = g_new (ShellPerfBlock, 1); + block->bytes = 0; + g_queue_push_tail (perf_log->blocks, block); + } + else + { + block = (ShellPerfBlock *)perf_log->blocks->tail->data; + } + + pos = block->bytes; + + memcpy (block->buffer + pos, &time_delta, sizeof (guint32)); + pos += sizeof (guint32); + memcpy (block->buffer + pos, &event->id, sizeof (guint16)); + pos += sizeof (guint16); + memcpy (block->buffer + pos, bytes, bytes_len); + pos += bytes_len; + + block->bytes = pos; +} + +/** + * shell_perf_log_event: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * + * Records a performance event with no arguments. + */ +void +shell_perf_log_event (ShellPerfLog *perf_log, + const char *name) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, ""); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, NULL, 0); +} + +/** + * shell_perf_log_event_i: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one 32-bit integer argument. + */ +void +shell_perf_log_event_i (ShellPerfLog *perf_log, + const char *name, + gint32 arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "i"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)&arg, sizeof (arg)); +} + +/** + * shell_perf_log_event_x: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one 64-bit integer argument. + */ +void +shell_perf_log_event_x (ShellPerfLog *perf_log, + const char *name, + gint64 arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "x"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)&arg, sizeof (arg)); +} + +/** + * shell_perf_log_event_s: + * @perf_log: a #ShellPerfLog + * @name: name of the event + * @arg: the argument + * + * Records a performance event with one string argument. + */ +void +shell_perf_log_event_s (ShellPerfLog *perf_log, + const char *name, + const char *arg) +{ + ShellPerfEvent *event = lookup_event (perf_log, name, "s"); + if (G_UNLIKELY (event == NULL)) + return; + + record_event (perf_log, get_time(), event, + (const guchar *)arg, strlen (arg) + 1); +} + +/** + * shell_perf_log_define_statistic: + * @name: name of the statistic and of the corresponding event. + * This should follow the same guidelines as for shell_perf_log_define_event() + * @description: human readable description of the statistic. + * @signature: The type of the data stored for statistic. Must + * currently be 'i' or 'x'. + * + * Defines a statistic. A statistic is a numeric value that is stored + * by the performance log and recorded periodically or when + * shell_perf_log_collect_statistics() is called explicitly. + * + * Code that defines a statistic should update it by calling + * the update function for the particular data type of the statistic, + * such as shell_perf_log_update_statistic_i(). This can be done + * at any time, but would normally done inside a function registered + * with shell_perf_log_add_statistics_callback(). These functions + * are called immediately before statistics are recorded. + */ +void +shell_perf_log_define_statistic (ShellPerfLog *perf_log, + const char *name, + const char *description, + const char *signature) +{ + ShellPerfEvent *event; + ShellPerfStatistic *statistic; + + if (strcmp (signature, "i") != 0 && + strcmp (signature, "x") != 0) + { + g_warning ("Only supported statistic signatures are 'i' and 'x'\n"); + return; + } + + event = define_event (perf_log, name, description, signature); + if (event == NULL) + return; + + statistic = g_new (ShellPerfStatistic, 1); + statistic->event = event; + + statistic->initialized = FALSE; + statistic->recorded = FALSE; + + g_ptr_array_add (perf_log->statistics, statistic); + g_hash_table_insert (perf_log->statistics_by_name, event->name, statistic); +} + +static ShellPerfStatistic * +lookup_statistic (ShellPerfLog *perf_log, + const char *name, + const char *signature) +{ + ShellPerfStatistic *statistic = g_hash_table_lookup (perf_log->statistics_by_name, name); + + if (G_UNLIKELY (statistic == NULL)) + { + g_warning ("Unknown statistic '%s'\n", name); + return NULL; + } + + if (G_UNLIKELY (strcmp (statistic->event->signature, signature) != 0)) + { + g_warning ("Statistic '%s'; defined with signature '%s', used with '%s'\n", + name, statistic->event->signature, signature); + return NULL; + } + + return statistic; +} + +/** + * shell_perf_log_update_statistic_i: + * @perf_log: a #ShellPerfLog + * @name: name of the statistic + * @value: new value for the statistic + * + * Updates the current value of an 32-bit integer statistic. + */ +void +shell_perf_log_update_statistic_i (ShellPerfLog *perf_log, + const char *name, + gint32 value) +{ + ShellPerfStatistic *statistic; + + statistic = lookup_statistic (perf_log, name, "i"); + if (G_UNLIKELY (statistic == NULL)) + return; + + statistic->current_value.i = value; + statistic->initialized = TRUE; +} + +/** + * shell_perf_log_update_statistic_x: + * @perf_log: a #ShellPerfLog + * @name: name of the statistic + * @value: new value for the statistic + * + * Updates the current value of an 64-bit integer statistic. + */ +void +shell_perf_log_update_statistic_x (ShellPerfLog *perf_log, + const char *name, + gint64 value) +{ + ShellPerfStatistic *statistic; + + statistic = lookup_statistic (perf_log, name, "x"); + if (G_UNLIKELY (statistic == NULL)) + return; + + statistic->current_value.x = value; + statistic->initialized = TRUE; +} + +/** + * shell_perf_log_add_statistics_callback: + * @perf_log: a #ShellPerfLog + * @callback: function to call before recording statistics + * @user_data: data to pass to @callback + * @notify: function to call when @user_data is no longer needed + * + * Adds a function that will be called before statistics are recorded. + * The function would typically compute one or more statistics values + * and call a function such as shell_perf_log_update_statistic_i() + * to update the value that will be recorded. + */ +void +shell_perf_log_add_statistics_callback (ShellPerfLog *perf_log, + ShellPerfStatisticsCallback callback, + gpointer user_data, + GDestroyNotify notify) +{ + ShellPerfStatisticsClosure *closure = g_new (ShellPerfStatisticsClosure, 1); + + closure->callback = callback; + closure->user_data = user_data; + closure->notify = notify; + + g_ptr_array_add (perf_log->statistics_closures, closure); +} + +/** + * shell_perf_log_collect_statistics: + * @perf_log: a #ShellPerfLog + * + * Calls all the update functions added with + * shell_perf_log_add_statistics_callback() and then records events + * for all statistics, followed by a perf.statisticsCollected event. + */ +void +shell_perf_log_collect_statistics (ShellPerfLog *perf_log) +{ + gint64 event_time = get_time (); + gint64 collection_time; + guint i; + + if (!perf_log->enabled) + return; + + for (i = 0; i < perf_log->statistics_closures->len; i++) + { + ShellPerfStatisticsClosure *closure; + + closure = g_ptr_array_index (perf_log->statistics_closures, i); + closure->callback (perf_log, closure->user_data); + } + + collection_time = get_time() - event_time; + + for (i = 0; i < perf_log->statistics->len; i++) + { + ShellPerfStatistic *statistic = g_ptr_array_index (perf_log->statistics, i); + + if (!statistic->initialized) + continue; + + switch (statistic->event->signature[0]) + { + case 'i': + if (!statistic->recorded || + statistic->current_value.i != statistic->last_value.i) + { + record_event (perf_log, event_time, statistic->event, + (const guchar *)&statistic->current_value.i, + sizeof (gint32)); + statistic->last_value.i = statistic->current_value.i; + statistic->recorded = TRUE; + } + break; + case 'x': + if (!statistic->recorded || + statistic->current_value.x != statistic->last_value.x) + { + record_event (perf_log, event_time, statistic->event, + (const guchar *)&statistic->current_value.x, + sizeof (gint64)); + statistic->last_value.x = statistic->current_value.x; + statistic->recorded = TRUE; + } + break; + default: + g_warning ("Unsupported signature in event"); + break; + } + } + + record_event (perf_log, event_time, + g_ptr_array_index (perf_log->events, EVENT_STATISTICS_COLLECTED), + (const guchar *)&collection_time, sizeof (gint64)); +} + +/** + * shell_perf_log_replay: + * @perf_log: a #ShellPerfLog + * @replay_function: (scope call): function to call for each event in the log + * @user_data: data to pass to @replay_function + * + * Replays the log by calling the given function for each event + * in the log. + */ +void +shell_perf_log_replay (ShellPerfLog *perf_log, + ShellPerfReplayFunction replay_function, + gpointer user_data) +{ + gint64 event_time = perf_log->start_time; + GList *iter; + + for (iter = perf_log->blocks->head; iter; iter = iter->next) + { + ShellPerfBlock *block = iter->data; + guint32 pos = 0; + + while (pos < block->bytes) + { + ShellPerfEvent *event; + guint16 id; + guint32 time_delta; + GValue arg = { 0, }; + + memcpy (&time_delta, block->buffer + pos, sizeof (guint32)); + pos += sizeof (guint32); + memcpy (&id, block->buffer + pos, sizeof (guint16)); + pos += sizeof (guint16); + + if (id == EVENT_SET_TIME) + { + /* Internal, we don't include in the replay */ + memcpy (&event_time, block->buffer + pos, sizeof (gint64)); + pos += sizeof (gint64); + continue; + } + else + { + event_time += time_delta; + } + + event = g_ptr_array_index (perf_log->events, id); + + if (strcmp (event->signature, "") == 0) + { + /* We need to pass something, so pass an empty string */ + g_value_init (&arg, G_TYPE_STRING); + } + else if (strcmp (event->signature, "i") == 0) + { + gint32 l; + + memcpy (&l, block->buffer + pos, sizeof (gint32)); + pos += sizeof (gint32); + + g_value_init (&arg, G_TYPE_INT); + g_value_set_int (&arg, l); + } + else if (strcmp (event->signature, "x") == 0) + { + gint64 l; + + memcpy (&l, block->buffer + pos, sizeof (gint64)); + pos += sizeof (gint64); + + g_value_init (&arg, G_TYPE_INT64); + g_value_set_int64 (&arg, l); + } + else if (strcmp (event->signature, "s") == 0) + { + g_value_init (&arg, G_TYPE_STRING); + g_value_set_string (&arg, (char *)block->buffer + pos); + pos += strlen ((char *)(block->buffer + pos)) + 1; + } + + replay_function (event_time, event->name, event->signature, &arg, user_data); + g_value_unset (&arg); + } + } +} + +static char * +escape_quotes (const char *input) +{ + GString *result; + const char *p; + + if (strchr (input, '"') == NULL) + return (char *)input; + + result = g_string_new (NULL); + for (p = input; *p; p++) + { + if (*p == '"') + g_string_append (result, "\\\""); + else + g_string_append_c (result, *p); + } + + return g_string_free (result, FALSE); +} + +static gboolean +write_string (GOutputStream *out, + const char *str, + GError **error) +{ + return g_output_stream_write_all (out, str, strlen (str), + NULL, NULL, + error); +} + +/** + * shell_perf_log_dump_events: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event definitions + * @error: location to store #GError, or %NULL + * + * Dump the definition of currently defined events and statistics, formatted + * as JSON, to the specified output stream. The JSON output is an array, + * with each element being a dictionary of the form: + * + * { name: , + * description: events->len; i++) + { + ShellPerfEvent *event = g_ptr_array_index (perf_log->events, i); + char *escaped_description = escape_quotes (event->description); + gboolean is_statistic = g_hash_table_lookup (perf_log->statistics_by_name, event->name) != NULL; + + if (i != 0) + g_string_append (output, ",\n "); + + g_string_append_printf (output, + "{ \"name\": \"%s\",\n" + " \"description\": \"%s\"", + event->name, escaped_description); + if (is_statistic) + g_string_append (output, ",\n \"statistic\": true"); + + g_string_append (output, " }"); + + if (escaped_description != event->description) + g_free (escaped_description); + } + + g_string_append (output, " ]"); + + return write_string (out, g_string_free (output, FALSE), error); +} + +typedef struct { + GOutputStream *out; + GError *error; + gboolean first; +} ReplayToJsonClosure; + +static void +replay_to_json (gint64 time, + const char *name, + const char *signature, + GValue *arg, + gpointer user_data) +{ + ReplayToJsonClosure *closure = user_data; + g_autofree char *event_str = NULL; + + if (closure->error != NULL) + return; + + if (!closure->first) + { + if (!write_string (closure->out, ",\n ", &closure->error)) + return; + } + + closure->first = FALSE; + + if (strcmp (signature, "") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\"]", time, name); + } + else if (strcmp (signature, "i") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %i]", + time, + name, + g_value_get_int (arg)); + } + else if (strcmp (signature, "x") == 0) + { + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", %"G_GINT64_FORMAT "]", + time, + name, + g_value_get_int64 (arg)); + } + else if (strcmp (signature, "s") == 0) + { + const char *arg_str = g_value_get_string (arg); + char *escaped = escape_quotes (arg_str); + + event_str = g_strdup_printf ("[%" G_GINT64_FORMAT ", \"%s\", \"%s\"]", + time, + name, + g_value_get_string (arg)); + + if (escaped != arg_str) + g_free (escaped); + } + else + { + g_assert_not_reached (); + } + + if (!write_string (closure->out, event_str, &closure->error)) + return; +} + +/** + * shell_perf_log_dump_log: + * @perf_log: a #ShellPerfLog + * @out: output stream into which to write the event log + * @error: location to store #GError, or %NULL + * + * Writes the performance event log, formatted as JSON, to the specified + * output stream. For performance reasons, the output stream passed + * in should generally be a buffered (or memory) output stream, since + * it will be written to in small pieces. The JSON output is an array + * with the elements of the array also being arrays, of the form + * '['