diff --git a/js/ui/altTab.js b/js/ui/altTab.js index d063cab21..62b95e327 100644 --- a/js/ui/altTab.js +++ b/js/ui/altTab.js @@ -59,8 +59,8 @@ AltTabPopup.prototype = { }, show : function(backward) { - let appMonitor = Shell.AppMonitor.get_default(); - let apps = appMonitor.get_running_apps (""); + let tracker = Shell.WindowTracker.get_default(); + let apps = tracker.get_running_apps (""); if (!apps.length) return false; diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index e3da4854a..1ded1bc61 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -13,6 +13,7 @@ const Mainloop = imports.mainloop; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; +const AppFavorites = imports.ui.appFavorites; const AppIcon = imports.ui.appIcon; const DND = imports.ui.dnd; const GenericDisplay = imports.ui.genericDisplay; @@ -57,7 +58,9 @@ AppDisplayItem.prototype = { // Opens an application represented by this display item. launch : function() { - let windows = Shell.AppMonitor.get_default().get_windows_for_app(this._appInfo.get_id()); + let appSys = Shell.AppSystem.get_default(); + let app = appSys.get_app(this._appInfo.get_id()); + let windows = app.get_windows(); if (windows.length > 0) { let mostRecentWindow = windows[0]; Main.overview.activateWindow(mostRecentWindow, Main.currentTime()); @@ -181,17 +184,12 @@ AppDisplay.prototype = { // We use a map of appIds instead of an array to ensure that we don't have duplicates and for easier lookup. this._menuSearchAppMatches = {}; - this._appMonitor = Shell.AppMonitor.get_default(); this._appSystem = Shell.AppSystem.get_default(); this._appsStale = true; this._appSystem.connect('installed-changed', Lang.bind(this, function(appSys) { this._appsStale = true; this._redisplay(GenericDisplay.RedisplayFlags.NONE); })); - this._appSystem.connect('favorites-changed', Lang.bind(this, function(appSys) { - this._appsStale = true; - this._redisplay(GenericDisplay.RedisplayFlags.NONE); - })); this._focusInMenus = true; this._activeMenuIndex = -1; @@ -301,9 +299,8 @@ AppDisplay.prototype = { _getMostUsed: function() { let context = ""; - return this._appMonitor.get_most_used_apps(context, 30).map(Lang.bind(this, function (id) { - return this._appSystem.lookup_cached_app(id); - })).filter(function (e) { return e != null }); + let usage = Shell.AppUsage.get_default(); + return usage.get_most_used(context, 30); }, _addMenuItem: function(name, id, index) { @@ -523,7 +520,7 @@ BaseWellItem.prototype = { // as say Pidgin, but ideally what we do there is have the app // express to us that it doesn't do relaunch=new-window in the // .desktop file. - this.app.get_info().launch(); + this.app.launch(); }, getDragActor: function() { @@ -557,7 +554,7 @@ RunningWellItem.prototype = { let modifiers = Shell.get_event_state(event); if (modifiers & Clutter.ModifierType.CONTROL_MASK) { - this.app.get_info().launch(); + this.app.launch(); } else { this.activateMostRecentWindow(); } @@ -611,7 +608,7 @@ InactiveWellItem.prototype = { }, _onActivate: function() { - this.app.get_info().launch(); + this.app.launch(); Main.overview.hide(); return true; }, @@ -791,6 +788,8 @@ AppWell.prototype = { this._menus = []; this._menuDisplays = []; + this._favorites = []; + this.actor = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL, x_align: Big.BoxAlignment.CENTER }); this.actor._delegate = this; @@ -801,21 +800,13 @@ AppWell.prototype = { this._grid = new WellGrid(); this.actor.append(this._grid.actor, Big.BoxPackFlags.EXPAND); + this._tracker = Shell.WindowTracker.get_default(); this._appSystem = Shell.AppSystem.get_default(); - this._appMonitor = Shell.AppMonitor.get_default(); - this._appSystem.connect('installed-changed', Lang.bind(this, function(appSys) { - this._redisplay(); - })); - this._appSystem.connect('favorites-changed', Lang.bind(this, function(appSys) { - this._redisplay(); - })); - this._appMonitor.connect('window-added', Lang.bind(this, function(monitor) { - this._redisplay(); - })); - this._appMonitor.connect('window-removed', Lang.bind(this, function(monitor) { - this._redisplay(); - })); + this._appSystem.connect('installed-changed', Lang.bind(this, this._redisplay)); + + AppFavorites.getAppFavorites().connect('changed', Lang.bind(this, this._redisplay)); + this._tracker.connect('app-running-changed', Lang.bind(this, this._redisplay)); this._redisplay(); }, @@ -843,17 +834,16 @@ AppWell.prototype = { this._grid.removeAll(); - let favorites = this._appMonitor.get_favorites(); - let favoriteIds = this._appIdListToHash(favorites); + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); /* hardcode here pending some design about how exactly desktop contexts behave */ let contextId = ""; - let running = this._appMonitor.get_running_apps(contextId); + let running = this._tracker.get_running_apps(contextId); let runningIds = this._appIdListToHash(running); - for (let i = 0; i < favorites.length; i++) { - let app = favorites[i]; + for (let id in favorites) { + let app = favorites[id]; let display; if (app.get_windows().length > 0) { display = new RunningWellItem(app, true); @@ -865,7 +855,7 @@ AppWell.prototype = { for (let i = 0; i < running.length; i++) { let app = running[i]; - if (app.get_id() in favoriteIds) + if (app.get_id() in favorites) continue; let display = new RunningWellItem(app, false); this._grid.actor.add_actor(display.actor); @@ -874,14 +864,11 @@ AppWell.prototype = { // Draggable target interface acceptDrop : function(source, actor, x, y, time) { - let appSystem = Shell.AppSystem.get_default(); - let app = null; if (source instanceof AppDisplayItem) { - app = appSystem.lookup_cached_app(source.getId()); + app = this._appSystem.get_app(source.getId()); } else if (source instanceof Workspaces.WindowClone) { - let appMonitor = Shell.AppMonitor.get_default(); - app = appMonitor.get_window_app(source.metaWindow); + app = this._tracker.get_window_app(source.metaWindow); } // Don't allow favoriting of transient apps @@ -891,18 +878,17 @@ AppWell.prototype = { let id = app.get_id(); - let favorites = this._appMonitor.get_favorites(); - let favoriteIds = this._appIdListToHash(favorites); + let favorites = AppFavorites.getAppFavorites().getFavoriteMap(); - let srcIsFavorite = (id in favoriteIds); + let srcIsFavorite = (id in favorites); if (srcIsFavorite) { return false; } else { - Mainloop.idle_add(function () { - appSystem.add_favorite(id); + Mainloop.idle_add(Lang.bind(this, function () { + AppFavorites.getAppFavorites().addFavorite(id); return false; - }); + })); } return true; diff --git a/js/ui/appFavorites.js b/js/ui/appFavorites.js new file mode 100644 index 000000000..3f32c0b66 --- /dev/null +++ b/js/ui/appFavorites.js @@ -0,0 +1,90 @@ +/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */ + +const Shell = imports.gi.Shell; +const Lang = imports.lang; +const Signals = imports.signals; + + +function AppFavorites() { + this._init(); +} + +AppFavorites.prototype = { + FAVORITE_APPS_KEY: 'favorite_apps', + + _init: function() { + this._favorites = {}; + this._gconf = Shell.GConf.get_default(); + this._gconf.connect('changed::' + this.FAVORITE_APPS_KEY, Lang.bind(this, this._onFavsChanged)); + this._reload(); + }, + + _onFavsChanged: function() { + this._reload(); + this.emit('changed'); + }, + + _reload: function() { + let ids = Shell.GConf.get_default().get_string_list('favorite_apps'); + let appSys = Shell.AppSystem.get_default(); + let apps = ids.map(function (id) { + return appSys.get_app(id); + }).filter(function (app) { + return app != null; + }); + this._favorites = {}; + for (let i = 0; i < apps.length; i++) { + let app = apps[i]; + this._favorites[app.get_id()] = app; + } + }, + + _getIds: function() { + let ret = []; + for (let id in this._favorites) + ret.push(id); + return ret; + }, + + getFavoriteMap: function() { + return this._favorites; + }, + + getFavorites: function() { + let ret = []; + for (let id in this._favorites) + ret.push(this._favorites[id]); + return ret; + }, + + isFavorite: function(appId) { + return appId in this._favorites; + }, + + addFavorite: function(appId) { + if (appId in this._favorites) + return; + let app = Shell.AppSystem.get_default().get_app(appId); + if (!app) + return; + let ids = this._getIds(); + ids.push(appId); + this._gconf.set_string_list(this.FAVORITE_APPS_KEY, ids); + this._favorites[appId] = app; + }, + + removeFavorite: function(appId) { + if (!appId in this._favorites) + return; + let ids = this._getIds().filter(function (id) { return id != appId; }); + this._gconf.set_string_list(this.FAVORITE_APPS_KEY, ids); + } +}; +Signals.addSignalMethods(AppFavorites.prototype); + +var appFavoritesInstance = null; +function getAppFavorites() { + if (appFavoritesInstance == null) + appFavoritesInstance = new AppFavorites(); + return appFavoritesInstance; +} diff --git a/js/ui/appIcon.js b/js/ui/appIcon.js index cc7bfc104..ab8ede104 100644 --- a/js/ui/appIcon.js +++ b/js/ui/appIcon.js @@ -12,6 +12,7 @@ const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; const GenericDisplay = imports.ui.genericDisplay; +const AppFavorites = imports.ui.appFavorites; const Main = imports.ui.main; const Workspaces = imports.ui.workspaces; @@ -404,20 +405,13 @@ AppIconMenu.prototype = { if (windows.length > 0) this._appendSeparator(); + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + this._newWindowMenuItem = windows.length > 0 ? this._appendMenuItem(null, _("New Window")) : null; - let favorites = Shell.AppSystem.get_default().get_favorites(); - let id = this._source.app.get_id(); - this._isFavorite = false; - for (let i = 0; i < favorites.length; i++) { - if (id == favorites[i]) { - this._isFavorite = true; - break; - } - } if (windows.length > 0) this._appendSeparator(); - this._toggleFavoriteMenuItem = this._appendMenuItem(null, this._isFavorite ? _("Remove from Favorites") + this._toggleFavoriteMenuItem = this._appendMenuItem(null, isFavorite ? _("Remove from Favorites") : _("Add to Favorites")); this._highlightedItem = null; @@ -560,14 +554,14 @@ AppIconMenu.prototype = { let metaWindow = child._window; this.emit('activate-window', metaWindow); } else if (child == this._newWindowMenuItem) { - this._source.app.get_info().launch(); + this._source.app.launch(); this.emit('activate-window', null); } else if (child == this._toggleFavoriteMenuItem) { - let appSys = Shell.AppSystem.get_default(); - if (this._isFavorite) - appSys.remove_favorite(this._source.app.get_id()); + let isFavorite = AppFavorites.getAppFavorites().isFavorite(this._source.app.get_id()); + if (isFavorite) + favs.removeFavorite(this._source.app.get_id()); else - appSys.add_favorite(this._source.app.get_id()); + favs.addFavorite(this._source.app.get_id()); } this.popdown(); }, diff --git a/js/ui/main.js b/js/ui/main.js index 623584692..aac26720b 100644 --- a/js/ui/main.js +++ b/js/ui/main.js @@ -55,15 +55,16 @@ function start() { Environment.init(); - // Ensure ShellAppMonitor is initialized; this will + // Ensure ShellWindowTracker and ShellAppUsage are initialized; this will // also initialize ShellAppSystem first. ShellAppSystem - // needs to load all the .desktop files, and ShellAppMonitor + // needs to load all the .desktop files, and ShellWindowTracker // will use those to associate with windows. Right now // the Monitor doesn't listen for installed app changes // and recalculate application associations, so to avoid // races for now we initialize it here. It's better to // be predictable anyways. - Shell.AppMonitor.get_default(); + Shell.WindowTracker.get_default(); + Shell.AppUsage.get_default(); // The background color really only matters if there is no desktop // window (say, nautilus) running. We set it mostly so things look good diff --git a/js/ui/panel.js b/js/ui/panel.js index 9b481236c..1365b663a 100644 --- a/js/ui/panel.js +++ b/js/ui/panel.js @@ -109,34 +109,26 @@ AppPanelMenu.prototype = { this.actor.opacity = 192; })); - this._metaDisplay.connect('notify::focus-window', Lang.bind(this, this._sync)); - - let appMonitor = Shell.AppMonitor.get_default(); - appMonitor.connect('startup-sequence-changed', Lang.bind(this, this._sync)); - // For now just resync on application add/remove; this is mainly to handle + let tracker = Shell.WindowTracker.get_default(); + tracker.connect('notify::focus-app', Lang.bind(this, this._sync)); + tracker.connect('startup-sequence-changed', Lang.bind(this, this._sync)); + // For now just resync on all running state changes; this is mainly to handle // cases where the focused window's application changes without the focus // changing. An example case is how we map Firefox based on the window // title which is a dynamic property. - appMonitor.connect('app-added', Lang.bind(this, this._sync)); - appMonitor.connect('app-removed', Lang.bind(this, this._sync)); + tracker.connect('app-running-changed', Lang.bind(this, this._sync)); this._sync(); }, _sync: function() { - let appMonitor = Shell.AppMonitor.get_default(); + let tracker = Shell.WindowTracker.get_default(); - let focusWindow = this._metaDisplay.get_focus_window(); - let focusedApp; - if (focusWindow == null) { - focusedApp = null; - } else { - focusedApp = appMonitor.get_window_app(focusWindow); - } + let focusedApp = tracker.focus_app; let lastSequence = null; if (focusedApp == null) { - let sequences = appMonitor.get_startup_sequences(); + let sequences = tracker.get_startup_sequences(); if (sequences.length > 0) lastSequence = sequences[sequences.length - 1]; } @@ -156,11 +148,10 @@ AppPanelMenu.prototype = { this._iconBox.hide(); this._label.text = ''; if (this._focusedApp != null) { - let info = focusedApp.get_info(); - let icon = info.create_icon_texture(PANEL_ICON_SIZE); + let icon = this._focusedApp.create_icon_texture(PANEL_ICON_SIZE); this._iconBox.append(icon, Big.BoxPackFlags.NONE); this._iconBox.show(); - this._label.text = info.get_name(); + this._label.text = this._focusedApp.get_name(); } else if (this._activeSequence != null) { let icon = this._activeSequence.create_icon(PANEL_ICON_SIZE); this._iconBox.append(icon, Big.BoxPackFlags.NONE); diff --git a/js/ui/widget.js b/js/ui/widget.js index cd8fd4276..341833d82 100644 --- a/js/ui/widget.js +++ b/js/ui/widget.js @@ -12,6 +12,7 @@ const Signals = imports.signals; const Gettext = imports.gettext.domain('gnome-shell'); const _ = Gettext.gettext; +const AppFavorites = imports.ui.appFavorites; const DocInfo = imports.misc.docInfo; const COLLAPSED_WIDTH = 24; @@ -318,12 +319,9 @@ AppsWidget.prototype = { this.collapsedActor = new Big.Box({ spacing: 2}); let appSystem = Shell.AppSystem.get_default(); - let apps = appSystem.get_favorites(); + let apps = AppFavorites.getAppFavorites().getFavorites(); for (let i = 0; i < apps.length; i++) { - let app = appSystem.lookup_cached_app(apps[i]); - if (!app) - continue; - this.addItem(new AppsWidgetInfo(app)); + this.addItem(new AppsWidgetInfo(apps[i])); } } }; diff --git a/js/ui/workspaces.js b/js/ui/workspaces.js index 3be0ab39b..1cde8acce 100644 --- a/js/ui/workspaces.js +++ b/js/ui/workspaces.js @@ -825,7 +825,6 @@ Workspace.prototype = { * fashion by the order in which the windows were created. */ _orderWindowsByMotionAndStartup: function(windows, slots) { - let appMonitor = Shell.AppMonitor.get_default(); windows.sort(function(w1, w2) { return w2.get_stable_sequence() - w1.get_stable_sequence(); }); @@ -1220,14 +1219,13 @@ Workspace.prototype = { // Tests if @win should be shown in the Overview _isOverviewWindow : function (win) { - let appMon = Shell.AppMonitor.get_default() - return appMon.is_window_usage_tracked(win.get_meta_window()); + let tracker = Shell.WindowTracker.get_default() + return tracker.is_window_interesting(win.get_meta_window()); }, _createWindowIcon: function(window) { - let appSys = Shell.AppSystem.get_default(); - let appMon = Shell.AppMonitor.get_default() - let app = appMon.get_window_app(window.metaWindow); + let tracker = Shell.WindowTracker.get_default() + let app = tracker.get_window_app(window.metaWindow); let iconTexture = null; // The design is application based, so prefer the application // icon here if we have it. FIXME - should move this fallback code @@ -1458,10 +1456,11 @@ Workspaces.prototype = { this._windowSelectionAppId = appId; - let appSys = Shell.AppMonitor.get_default(); + let appSys = Shell.AppSystem.get_default(); let showOnlyWindows = {}; - let windows = appSys.get_windows_for_app(appId); + let app = appSys.get_app(appId); + let windows = app.get_windows(); for (let i = 0; i < windows.length; i++) { showOnlyWindows[windows[i]] = 1; } diff --git a/src/Makefile.am b/src/Makefile.am index faa625673..5290748df 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -56,10 +56,11 @@ libgnome_shell_la_SOURCES = \ gnome-shell-plugin.c \ shell-app.c \ shell-app.h \ - shell-app-monitor.c \ - shell-app-monitor.h \ + shell-app-private.h \ shell-app-system.c \ shell-app-system.h \ + shell-app-usage.c \ + shell-app-usage.h \ shell-arrow.c \ shell-arrow.h \ shell-button-box.c \ @@ -95,6 +96,8 @@ libgnome_shell_la_SOURCES = \ shell-texture-cache.h \ shell-uri-util.c \ shell-uri-util.h \ + shell-window-tracker.c \ + shell-window-tracker.h \ shell-wm.c \ shell-wm.h diff --git a/src/shell-app-monitor.c b/src/shell-app-monitor.c deleted file mode 100644 index 248b87ca2..000000000 --- a/src/shell-app-monitor.c +++ /dev/null @@ -1,1814 +0,0 @@ -/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include - -#define SN_API_NOT_YET_FROZEN 1 -#include - -#include "shell-texture-cache.h" -#include "shell-app-monitor.h" -#include "shell-app-system.h" -#include "shell-global.h" -#include "shell-marshal.h" - -#include "display.h" -#include "window.h" -#include "group.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-monitor - * @short_description: Associate windows with application data and track usage/state data - * - * The application monitor has two primary purposes. First, it - * maintains a mapping from windows to applications (.desktop file ids). - * It currently implements this with some heuristics on the WM_CLASS X11 - * property (and some static override regexps); in the future, we want to - * have it also track through startup-notification. - * - * Second, the monitor also maintains some usage and state statistics for - * windows by keeping track of the approximate time an application's - * windows are focus, as well as the last workspace it was seen on. - * This time tracking is implemented by watching for restack notifications, - * and computing a time delta between them. Also we monitor the - * GNOME Session "StatusChanged" signal which by default is emitted after 5 - * minutes to signify idle. - */ - -#define APP_MONITOR_GCONF_DIR SHELL_GCONF_DIR"/app_monitor" -#define ENABLE_MONITORING_KEY APP_MONITOR_GCONF_DIR"/enable_monitoring" - -#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 */ - -#define SNAPSHOTS_PER_CLEAN_SECONDS ((USAGE_CLEAN_DAYS * 24 * 60 * 60) / MIN_SNAPSHOT_APP_SECONDS) /* One week */ - -/* 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 /* leave this low for testing, we can bump later if need be */ - -/* 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) - -/* Title patterns to detect apps that don't set WM class as needed. - * Format: pseudo/wanted WM class, title regex pattern, NULL (for GRegex) */ -static struct -{ - const char *app_id; - const char *pattern; - GRegex *regex; -} title_patterns[] = { - {"mozilla-firefox.desktop", ".* - Mozilla Firefox", NULL}, \ - {"openoffice.org-writer.desktop", ".* - OpenOffice.org Writer$", NULL}, \ - {"openoffice.org-calc.desktop", ".* - OpenOffice.org Calc$", NULL}, \ - {"openoffice.org-impress.desktop", ".* - OpenOffice.org Impress$", NULL}, \ - {"openoffice.org-draw.desktop", ".* - OpenOffice.org Draw$", NULL}, \ - {"openoffice.org-base.desktop", ".* - OpenOffice.org Base$", NULL}, \ - {"openoffice.org-math.desktop", ".* - OpenOffice.org Math$", NULL}, \ - {NULL, NULL, NULL} -}; - - -typedef struct AppUsage AppUsage; -typedef struct ActiveAppsData ActiveAppsData; - -struct _ShellAppMonitor -{ - GObject parent; - - GFile *configfile; - DBusGProxy *session_proxy; - GdkDisplay *display; - GConfClient *gconf_client; - gulong last_idle; - guint idle_focus_change_id; - guint save_id; - guint gconf_notify; - gboolean currently_idle; - gboolean enable_monitoring; - - /* See comment in AppUsage below */ - guint initially_seen_sequence; - - GSList *previously_running; - - long watch_start_time; - MetaWindow *watched_window; - - /* */ - GHashTable *window_to_app; - - /* */ - GHashTable *running_apps; - - /* > */ - GHashTable *app_usages_for_context; -}; - -G_DEFINE_TYPE (ShellAppMonitor, shell_app_monitor, G_TYPE_OBJECT); - -/* Represents an application record for a given context */ -struct AppUsage -{ - /* Whether the application we're tracking is "transient", see - * shell_app_info_is_transient. - */ - gboolean transient; - - 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 */ - - /* how many windows are currently open; in terms of persistence we only save - * whether the app had any windows or not. */ - guint window_count; - - /* Transient data */ - guint initially_seen_sequence; /* Arbitrary ordered integer for when we first saw - * this application in this session. Used to order - * the open applications. - */ -}; - -enum { - APP_ADDED, - APP_REMOVED, - WINDOW_ADDED, - WINDOW_REMOVED, - STARTUP_SEQUENCE_CHANGED, - - LAST_SIGNAL -}; - -static guint signals[LAST_SIGNAL] = { 0 }; - -static void shell_app_monitor_finalize (GObject *object); - -static void on_session_status_changed (DBusGProxy *proxy, guint status, ShellAppMonitor *monitor); -static void on_focus_window_changed (MetaDisplay *display, GParamSpec *spec, ShellAppMonitor *monitor); -static void ensure_queued_save (ShellAppMonitor *monitor); -static AppUsage * get_app_usage_for_context_and_id (ShellAppMonitor *monitor, - const char *context, - const char *appid); - -static void track_window (ShellAppMonitor *monitor, MetaWindow *window); -static void disassociate_window (ShellAppMonitor *monitor, MetaWindow *window); - -static gboolean idle_save_application_usage (gpointer data); - -static void restore_from_file (ShellAppMonitor *monitor); - -static void update_enable_monitoring (ShellAppMonitor *monitor); - -static void on_enable_monitoring_key_changed (GConfClient *client, - guint connexion_id, - GConfEntry *entry, - gpointer monitor); - -static long -get_time (void) -{ - GTimeVal tv; - g_get_current_time (&tv); - return tv.tv_sec; -} - -static void shell_app_monitor_class_init(ShellAppMonitorClass *klass) -{ - GObjectClass *gobject_class = G_OBJECT_CLASS (klass); - - gobject_class->finalize = shell_app_monitor_finalize; - - signals[APP_ADDED] = g_signal_new ("app-added", - SHELL_TYPE_APP_MONITOR, - G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, - g_cclosure_marshal_VOID__OBJECT, - G_TYPE_NONE, 1, - SHELL_TYPE_APP); - signals[APP_REMOVED] = g_signal_new ("app-removed", - SHELL_TYPE_APP_MONITOR, - G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, - g_cclosure_marshal_VOID__OBJECT, - G_TYPE_NONE, 1, - SHELL_TYPE_APP); - - signals[WINDOW_ADDED] = g_signal_new ("window-added", - SHELL_TYPE_APP_MONITOR, - G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, - _shell_marshal_VOID__OBJECT_OBJECT, - G_TYPE_NONE, 2, - SHELL_TYPE_APP, - META_TYPE_WINDOW); - signals[WINDOW_REMOVED] = g_signal_new ("window-removed", - SHELL_TYPE_APP_MONITOR, - G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, - _shell_marshal_VOID__OBJECT_OBJECT, - G_TYPE_NONE, 2, - SHELL_TYPE_APP, - META_TYPE_WINDOW); - - signals[STARTUP_SEQUENCE_CHANGED] = g_signal_new ("startup-sequence-changed", - SHELL_TYPE_APP_MONITOR, - G_SIGNAL_RUN_LAST, - 0, - NULL, NULL, - g_cclosure_marshal_VOID__BOXED, - G_TYPE_NONE, 1, SHELL_TYPE_STARTUP_SEQUENCE); -} - -static void -destroy_usage (AppUsage *usage) -{ - g_free (usage); -} - -/** - * get_app_id_from_title: - * - * Use a window's "title" property to determine an application ID. - * This is a temporary crutch for a few applications until we get - * them correctly setting their WM_CLASS. - */ -static const char * -get_app_id_from_title (MetaWindow *window) -{ - static gboolean patterns_initialized = FALSE; - char *title; - int i; - - g_object_get (window, "title", &title, NULL); - - if (!patterns_initialized) /* Generate match patterns once for all */ - { - patterns_initialized = TRUE; - for (i = 0; title_patterns[i].app_id; i++) - { - title_patterns[i].regex = g_regex_new (title_patterns[i].pattern, - 0, 0, NULL); - } - } - - /* Match window title patterns to identifiers for non-standard apps */ - if (title) - { - for (i = 0; title_patterns[i].app_id; i++) - { - if (g_regex_match (title_patterns[i].regex, title, 0, NULL)) - { - g_free (title); - /* Set a pseudo WM class, handled like true ones */ - return title_patterns[i].app_id; - } - } - } - - g_free (title); - return NULL; -} - -/** - * get_cleaned_wmclass_for_window: - * - * A "cleaned" wmclass is the WM_CLASS property of a window, - * after some transformations to turn it into a form - * somewhat more resilient to changes, such as lowercasing. - */ -static char * -get_cleaned_wmclass_for_window (MetaWindow *window) -{ - const char *wmclass; - char *cleaned_wmclass; - - wmclass = meta_window_get_wm_class (window); - if (!wmclass) - return NULL; - - cleaned_wmclass = g_utf8_strdown (wmclass, -1); - - /* This handles "Fedora Eclipse", probably others. - * Note g_strdelimit is modify-in-place. */ - g_strdelimit (cleaned_wmclass, " ", '-'); - - return cleaned_wmclass; -} - -/** - * window_is_tracked: - * - * Returns: %TRUE iff we want to scan this window for application association - */ -static gboolean -window_is_tracked (MetaWindow *window) -{ - if (meta_window_is_override_redirect (window)) - return FALSE; - - return TRUE; -} - -/** - * shell_app_monitor_is_window_usage_tracked: - * - * Determine if it makes sense to track the given window for application - * usage. An example of a window we don't want to track is the root - * desktop window. We skip all override-redirect types, and also - * exclude other window types like tooltip explicitly, though generally - * most of these should be override-redirect. - * - * The usage data is also currently used to return the list of user-interesting - * windows associated with an application. - * - * Returns: %TRUE iff we want to record focus time spent in this window - */ -gboolean -shell_app_monitor_is_window_usage_tracked (MetaWindow *window) -{ - if (!window_is_tracked (window)) - return FALSE; - - if (meta_window_is_skip_taskbar (window)) - return FALSE; - - switch (meta_window_get_window_type (window)) - { - /* Definitely ignore these. */ - case META_WINDOW_DESKTOP: - case META_WINDOW_DOCK: - case META_WINDOW_SPLASHSCREEN: - /* Should have already been handled by override_redirect above, - * but explicitly list here so we get the "unhandled enum" - * warning if in the future anything is added.*/ - case META_WINDOW_DROPDOWN_MENU: - case META_WINDOW_POPUP_MENU: - case META_WINDOW_TOOLTIP: - case META_WINDOW_NOTIFICATION: - case META_WINDOW_COMBO: - case META_WINDOW_DND: - case META_WINDOW_OVERRIDE_OTHER: - return FALSE; - case META_WINDOW_NORMAL: - case META_WINDOW_DIALOG: - case META_WINDOW_MODAL_DIALOG: - case META_WINDOW_MENU: - case META_WINDOW_TOOLBAR: - case META_WINDOW_UTILITY: - break; - } - - return TRUE; -} - -/** - * get_app_for_window_direct: - * - * Looks only at the given window, and attempts to determine - * an application based on WM_CLASS. If that fails, then - * a "transient" application is created. - * - * Return value: (transfer full): A newly-referenced #ShellApp - */ -static ShellApp * -get_app_for_window_direct (MetaWindow *window) -{ - ShellApp *app; - ShellAppInfo *appinfo; - ShellAppSystem *appsys; - char *wmclass; - char *with_desktop; - - wmclass = get_cleaned_wmclass_for_window (window); - - if (!wmclass) - return _shell_app_new_for_window (window); - - with_desktop = g_strjoin (NULL, wmclass, ".desktop", NULL); - g_free (wmclass); - - appsys = shell_app_system_get_default (); - appinfo = shell_app_system_lookup_heuristic_basename (appsys, with_desktop); - g_free (with_desktop); - - if (appinfo == NULL) - { - const char *id = get_app_id_from_title (window); - - if (id != NULL) - appinfo = shell_app_system_load_from_desktop_file (appsys, id, NULL); - } - if (appinfo == NULL) - return _shell_app_new_for_window (window); - - app = _shell_app_new (appinfo); - shell_app_info_unref (appinfo); - return app; -} - -/** - * get_app_for_window: - * - * Determines the application associated with a window, using - * all available information such as the window's MetaGroup, - * and what we know about other windows. - */ -static ShellApp * -get_app_for_window (ShellAppMonitor *monitor, - MetaWindow *window) -{ - ShellApp *result; - MetaWindow *source_window; - GSList *group_windows; - MetaGroup *group; - GSList *iter; - - group = meta_window_get_group (window); - if (group == NULL) - group_windows = g_slist_prepend (NULL, window); - else - group_windows = meta_group_list_windows (group); - - source_window = window; - - result = NULL; - /* Try finding a window in the group of type NORMAL; if we - * succeed, use that as our source. */ - for (iter = group_windows; iter; iter = iter->next) - { - MetaWindow *group_window = iter->data; - - if (meta_window_get_window_type (group_window) != META_WINDOW_NORMAL) - continue; - - source_window = group_window; - result = g_hash_table_lookup (monitor->window_to_app, group_window); - if (result) - break; - } - - g_slist_free (group_windows); - - if (result != NULL) - { - g_object_ref (result); - return result; - } - - return get_app_for_window_direct (source_window); -} - -static const char * -get_window_context (MetaWindow *window) -{ - return ""; -} - -static const char * -get_app_context (ShellApp *app) -{ - return ""; -} - -static GHashTable * -get_usages_for_context (ShellAppMonitor *monitor, - const char *context) -{ - GHashTable *context_usages; - - context_usages = g_hash_table_lookup (monitor->app_usages_for_context, context); - if (context_usages == NULL) - { - context_usages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)destroy_usage); - g_hash_table_insert (monitor->app_usages_for_context, g_strdup (context), - context_usages); - } - return context_usages; -} - -static AppUsage * -get_app_usage_for_context_and_id (ShellAppMonitor *monitor, - const char *context, - const char *appid) -{ - AppUsage *usage; - GHashTable *context_usages; - - context_usages = get_usages_for_context (monitor, context); - - usage = g_hash_table_lookup (context_usages, appid); - if (usage) - return usage; - - usage = g_new0 (AppUsage, 1); - usage->initially_seen_sequence = ++monitor->initially_seen_sequence; - g_hash_table_insert (context_usages, g_strdup (appid), usage); - - return usage; -} - -static AppUsage * -get_app_usage_from_window (ShellAppMonitor *monitor, - MetaWindow *window) -{ - ShellApp *app; - const char *context; - - app = g_hash_table_lookup (monitor->window_to_app, window); - if (!app) - return NULL; - - context = get_window_context (window); - - return get_app_usage_for_context_and_id (monitor, context, shell_app_get_id (app)); -} - -static MetaWindow * -get_active_window (ShellAppMonitor *monitor) -{ - MetaScreen *screen; - MetaDisplay *display; - MetaWindow *window; - - screen = shell_global_get_screen (shell_global_get ()); - display = meta_screen_get_display (screen); - window = meta_display_get_focus_window (display); - - if (window != NULL && shell_app_monitor_is_window_usage_tracked (window)) - return window; - return NULL; -} - -typedef struct { - gboolean in_context; - GHashTableIter context_iter; - const char *context_id; - GHashTableIter usage_iter; -} UsageIterator; - -static void -usage_iterator_init (ShellAppMonitor *self, - UsageIterator *iter) -{ - iter->in_context = FALSE; - g_hash_table_iter_init (&(iter->context_iter), self->app_usages_for_context); -} - -static gboolean -usage_iterator_next (ShellAppMonitor *self, - UsageIterator *iter, - const char **context, - const char **id, - AppUsage **usage) -{ - gpointer key, value; - gboolean next_context; - - if (!iter->in_context) - next_context = TRUE; - else if (!g_hash_table_iter_next (&(iter->usage_iter), &key, &value)) - next_context = TRUE; - else - next_context = FALSE; - - while (next_context) - { - GHashTable *app_usages; - - if (!g_hash_table_iter_next (&(iter->context_iter), &key, &value)) - return FALSE; - iter->in_context = TRUE; - iter->context_id = key; - app_usages = value; - g_hash_table_iter_init (&(iter->usage_iter), app_usages); - - next_context = !g_hash_table_iter_next (&(iter->usage_iter), &key, &value); - } - - *context = iter->context_id; - *id = key; - *usage = value; - - return TRUE; -} - -static void -usage_iterator_remove (ShellAppMonitor *self, - UsageIterator *iter) -{ - g_assert (iter->in_context); - - g_hash_table_iter_remove (&(iter->usage_iter)); -} - -/* Limit the score to a certain level so that most used apps can change */ -static void -normalize_usage (ShellAppMonitor *self) -{ - UsageIterator iter; - const char *context; - const char *id; - AppUsage *usage; - - usage_iterator_init (self, &iter); - - while (usage_iterator_next (self, &iter, &context, &id, &usage)) - { - usage->score /= 2; - } -} - -static void -increment_usage_for_window_at_time (ShellAppMonitor *self, - MetaWindow *window, - long time) -{ - AppUsage *usage; - guint elapsed; - guint usage_count; - - usage = get_app_usage_from_window (self, window); - - 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_window (ShellAppMonitor *self, - MetaWindow *window) -{ - long curtime = get_time (); - increment_usage_for_window_at_time (self, window, curtime); -} - -/** - * reset_usage: - * - * For non-transient applications, just reset the "seen sequence". - * - * For transient ones, we don't want to keep an AppUsage around, so - * remove it entirely. - */ -static void -reset_usage (ShellAppMonitor *self, - const char *context, - const char *appid, - AppUsage *usage) -{ - if (!usage->transient) - { - usage->initially_seen_sequence = 0; - } - else - { - GHashTable *usages; - usages = g_hash_table_lookup (self->app_usages_for_context, context); - g_hash_table_remove (usages, appid); - } -} - -static void -on_transient_window_title_changed (MetaWindow *window, - GParamSpec *spec, - ShellAppMonitor *self) -{ - ShellAppSystem *appsys; - ShellAppInfo *new_app; - const char *id; - - /* Check if we now have a mapping using the window title */ - id = get_app_id_from_title (window); - if (id == NULL) - return; - - appsys = shell_app_system_get_default (); - new_app = shell_app_system_load_from_desktop_file (appsys, id, NULL); - if (new_app == NULL) - return; - shell_app_info_unref (new_app); - - /* It's simplest to just treat this as a remove + add. */ - disassociate_window (self, window); - track_window (self, window); -} - -static void -track_window (ShellAppMonitor *self, - MetaWindow *window) -{ - ShellApp *app; - AppUsage *usage; - - if (!window_is_tracked (window)) - return; - - app = get_app_for_window (self, window); - if (!app) - return; - - /* At this point we've stored the association from window -> application */ - g_hash_table_insert (self->window_to_app, window, app); - - /* However, we don't want to record usage for all kinds of windows; - * the desktop window is a prime example. If a window isn't usage - * tracked it doesn't count for the purposes of an application - * running. - */ - if (!shell_app_monitor_is_window_usage_tracked (window)) - return; - - usage = get_app_usage_from_window (self, window); - usage->transient = shell_app_info_is_transient (shell_app_get_info (app)); - - if (usage->transient) - { - /* For a transient application, it's possible one of our title regexps - * will match at a later time, i.e. the application may not have set - * its title fully at the time it initially maps a window. Watch - * for title changes and recompute the app. - */ - g_signal_connect (window, "notify::title", G_CALLBACK (on_transient_window_title_changed), self); - } - - _shell_app_add_window (app, window); - - /* Keep track of the number of windows open for this app, when it - * switches between 0 and 1 we emit an app-added signal. - */ - usage->window_count++; - if (usage->initially_seen_sequence == 0) - usage->initially_seen_sequence = ++self->initially_seen_sequence; - usage->last_seen = get_time (); - if (usage->window_count == 1) - { - /* key is owned by the app */ - g_hash_table_insert (self->running_apps, (char*)shell_app_get_id (app), - app); - g_signal_emit (self, signals[APP_ADDED], 0, app); - } - - /* Emit window-added after app-added */ - g_signal_emit (self, signals[WINDOW_ADDED], 0, app, window); -} - -static void -shell_app_monitor_on_window_added (MetaWorkspace *workspace, - MetaWindow *window, - gpointer user_data) -{ - ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); - - track_window (self, window); -} - -static void -disassociate_window (ShellAppMonitor *self, - MetaWindow *window) -{ - ShellApp *app; - - app = g_hash_table_lookup (self->window_to_app, window); - if (!app) - return; - - g_object_ref (app); - - if (window == self->watched_window) - self->watched_window = NULL; - - if (shell_app_monitor_is_window_usage_tracked (window)) - { - AppUsage *usage; - const char *context; - - context = get_window_context (window); - usage = get_app_usage_from_window (self, window); - usage->window_count--; - - g_hash_table_remove (self->window_to_app, window); - - _shell_app_remove_window (app, window); - - g_signal_emit (self, signals[WINDOW_REMOVED], 0, app, window); - - if (usage->window_count == 0) - { - const char *id = shell_app_get_id (app); - g_hash_table_remove (self->running_apps, id); - g_signal_emit (self, signals[APP_REMOVED], 0, app); - reset_usage (self, context, id, usage); - } - } - else - g_hash_table_remove (self->window_to_app, window); - - g_object_unref (app); -} - -static void -shell_app_monitor_on_window_removed (MetaWorkspace *workspace, - MetaWindow *window, - gpointer user_data) -{ - disassociate_window (SHELL_APP_MONITOR (user_data), window); -} - -static void -load_initial_windows (ShellAppMonitor *monitor) -{ - GList *workspaces, *iter; - MetaScreen *screen = shell_global_get_screen (shell_global_get ()); - workspaces = meta_screen_get_workspaces (screen); - - for (iter = workspaces; iter; iter = iter->next) - { - MetaWorkspace *workspace = iter->data; - GList *windows = meta_workspace_list_windows (workspace); - GList *window_iter; - - for (window_iter = windows; window_iter; window_iter = window_iter->next) - { - MetaWindow *window = window_iter->data; - track_window (monitor, window); - } - - g_list_free (windows); - } -} - -static void -on_session_status_changed (DBusGProxy *proxy, - guint status, - ShellAppMonitor *monitor) -{ - gboolean idle; - - idle = (status >= 3); - if (monitor->currently_idle == idle) - return; - - monitor->currently_idle = idle; - if (idle) - { - long end_time; - - /* Resync the active window, it may have changed while - * we were idle and ignoring focus changes. - */ - monitor->watched_window = get_active_window (monitor); - - /* 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 (monitor->watched_window) - { - end_time = monitor->watch_start_time + IDLE_TIME_TRANSITION_SECONDS; - increment_usage_for_window_at_time (monitor, monitor->watched_window, end_time); - } - } - else - { - /* Transitioning to !idle, reset the start time */ - monitor->watch_start_time = get_time (); - } -} - -/** - * shell_app_monitor_get_windows_for_app: - * @self: - * @appid: Find windows for this id - * - * Returns the normal toplevel windows associated with the given application. - * - * Returns: (transfer container) (element-type MetaWindow): List of #MetaWindow corresponding to appid - */ -GSList * -shell_app_monitor_get_windows_for_app (ShellAppMonitor *self, - const char *appid) -{ - GHashTableIter iter; - gpointer key, value; - GSList *ret = NULL; - - g_hash_table_iter_init (&iter, self->window_to_app); - - while (g_hash_table_iter_next (&iter, &key, &value)) - { - MetaWindow *window = key; - ShellApp *app = value; - const char *id = shell_app_get_id (app); - - if (!shell_app_monitor_is_window_usage_tracked (window)) - continue; - - if (strcmp (id, appid) != 0) - continue; - - ret = g_slist_prepend (ret, window); - } - return ret; -} - -static void -shell_app_monitor_on_n_workspaces_changed (MetaScreen *screen, - GParamSpec *pspec, - gpointer user_data) -{ - ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); - GList *workspaces, *iter; - - workspaces = meta_screen_get_workspaces (screen); - - for (iter = workspaces; iter; iter = iter->next) - { - MetaWorkspace *workspace = iter->data; - - /* This pair of disconnect/connect is idempotent if we were - * already connected, while ensuring we get connected for - * new workspaces. - */ - g_signal_handlers_disconnect_by_func (workspace, - shell_app_monitor_on_window_added, - self); - g_signal_handlers_disconnect_by_func (workspace, - shell_app_monitor_on_window_removed, - self); - - g_signal_connect (workspace, "window-added", - G_CALLBACK (shell_app_monitor_on_window_added), self); - g_signal_connect (workspace, "window-removed", - G_CALLBACK (shell_app_monitor_on_window_removed), self); - } -} - -static void -init_window_monitoring (ShellAppMonitor *self) -{ - MetaDisplay *display; - MetaScreen *screen = shell_global_get_screen (shell_global_get ()); - - g_signal_connect (screen, "notify::n-workspaces", - G_CALLBACK (shell_app_monitor_on_n_workspaces_changed), self); - display = meta_screen_get_display (screen); - g_signal_connect (display, "notify::focus-window", - G_CALLBACK (on_focus_window_changed), self); - - shell_app_monitor_on_n_workspaces_changed (screen, NULL, self); -} - -static void -on_startup_sequence_changed (MetaScreen *screen, - SnStartupSequence *sequence, - ShellAppMonitor *self) -{ - /* Just proxy the signal */ - g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence); -} - -static void -shell_app_monitor_init (ShellAppMonitor *self) -{ - MetaScreen *screen; - GdkDisplay *display; - char *path; - char *shell_config_dir; - DBusGConnection *session_bus; - - /* FIXME: should we create as many monitors as there are GdkScreens? */ - display = gdk_display_get_default (); - screen = shell_global_get_screen (shell_global_get ()); - - session_bus = dbus_g_bus_get (DBUS_BUS_SESSION, NULL); - self->session_proxy = dbus_g_proxy_new_for_name (session_bus, "org.gnome.SessionManager", - "/org/gnome/SessionManager/Presence", - "org.gnome.SessionManager"); - dbus_g_proxy_add_signal (self->session_proxy, "StatusChanged", - G_TYPE_UINT, G_TYPE_INVALID, G_TYPE_INVALID); - dbus_g_proxy_connect_signal (self->session_proxy, "StatusChanged", - G_CALLBACK (on_session_status_changed), self, NULL); - - self->display = g_object_ref (display); - - self->last_idle = 0; - self->currently_idle = FALSE; - self->enable_monitoring = FALSE; - - self->app_usages_for_context = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, - (GDestroyNotify) g_hash_table_destroy); - - self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, - NULL, (GDestroyNotify) g_object_unref); - - self->running_apps = g_hash_table_new (g_str_hash, g_str_equal); - - g_object_get (shell_global_get(), "configdir", &shell_config_dir, NULL), - path = g_build_filename (shell_config_dir, DATA_FILENAME, NULL); - g_free (shell_config_dir); - self->configfile = g_file_new_for_path (path); - g_free (path); - restore_from_file (self); - - load_initial_windows (self); - init_window_monitoring (self); - - g_signal_connect (G_OBJECT (screen), "startup-sequence-changed", - G_CALLBACK (on_startup_sequence_changed), self); - - self->gconf_client = gconf_client_get_default (); - gconf_client_add_dir (self->gconf_client, APP_MONITOR_GCONF_DIR, - GCONF_CLIENT_PRELOAD_NONE, NULL); - self->gconf_notify = - gconf_client_notify_add (self->gconf_client, ENABLE_MONITORING_KEY, - on_enable_monitoring_key_changed, self, NULL, NULL); - update_enable_monitoring (self); -} - -static void -shell_app_monitor_finalize (GObject *object) -{ - ShellAppMonitor *self = SHELL_APP_MONITOR (object); - int i; - - if (self->save_id > 0) - g_source_remove (self->save_id); - gconf_client_notify_remove (self->gconf_client, self->gconf_notify); - g_object_unref (self->gconf_client); - g_object_unref (self->display); - g_hash_table_destroy (self->running_apps); - g_hash_table_destroy (self->app_usages_for_context); - for (i = 0; title_patterns[i].app_id; i++) - g_regex_unref (title_patterns[i].regex); - g_object_unref (self->configfile); - - G_OBJECT_CLASS (shell_app_monitor_parent_class)->finalize(object); -} - -/** - * shell_app_monitor_get_most_used_apps: - * @monitor: the app monitor instance to request - * @context: Activity identifier - * @max_count: how many applications are requested. Note that the actual - * list size may be less, or NULL if not enough applications are registered. - * - * Get a list of desktop identifiers representing the most popular applications - * for a given context. - * - * Returns: (element-type utf8) (transfer container): List of application desktop - * identifiers - */ -GList * -shell_app_monitor_get_most_used_apps (ShellAppMonitor *monitor, - const char *context, - gint max_count) -{ - GHashTable *usages; - usages = g_hash_table_lookup (monitor->app_usages_for_context, context); - if (usages == NULL) - return NULL; - return g_hash_table_get_keys (usages); -} - -/** - * shell_app_monitor_get_window_app - * @monitor: An app monitor instance - * @metawin: A #MetaWindow - * - * Returns: Application associated with window - */ -ShellApp * -shell_app_monitor_get_window_app (ShellAppMonitor *monitor, - MetaWindow *metawin) -{ - MetaWindow *transient_for; - ShellApp *app; - - transient_for = meta_window_get_transient_for (metawin); - if (transient_for != NULL) - metawin = transient_for; - - app = g_hash_table_lookup (monitor->window_to_app, metawin); - if (app) - g_object_ref (app); - - return app; -} - -/** - * shell_app_monitor_get_running_apps: - * @monitor: An app monitor instance - * @context: Activity identifier - * - * Returns the set of applications which currently have at least one open - * window in the given context. The returned list will be sorted - * by shell_app_compare(). - * - * Returns: (element-type ShellApp) (transfer container): Active applications - */ -GSList * -shell_app_monitor_get_running_apps (ShellAppMonitor *monitor, - const char *context) -{ - gpointer key, value; - GSList *ret; - GHashTableIter iter; - - g_hash_table_iter_init (&iter, monitor->running_apps); - - ret = NULL; - while (g_hash_table_iter_next (&iter, &key, &value)) - { - ShellApp *app = value; - - if (strcmp (context, get_app_context (app)) != 0) - continue; - - ret = g_slist_prepend (ret, app); - } - - ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare); - - return ret; -} - -/** - * shell_app_monitor_get_app: - * @montior: - * @id: Application identifier - * - * For running applications, returns the existing instance - * of the running application model object. Otherwise, - * returns a new object. - * - * Returns: (transfer full): Application associated with id - */ -ShellApp * -shell_app_monitor_get_app (ShellAppMonitor *monitor, - const char *id) -{ - ShellApp *app; - ShellAppInfo *info; - - app = g_hash_table_lookup (monitor->running_apps, id); - if (app) - return g_object_ref (app); - - info = shell_app_system_lookup_cached_app (shell_app_system_get_default(), id); - if (!info) - return NULL; - - app = _shell_app_new (info); - shell_app_info_unref (info); - - return app; -} - -/** - * shell_app_monitor_get_favorites: - * @monitor: - * - * Returns: (transfer full) (element-type ShellApp): List of favorite applications - */ -GSList * -shell_app_monitor_get_favorites (ShellAppMonitor *monitor) -{ - GSList *apps = NULL; - GList *favorite_ids, *iter; - - favorite_ids = shell_app_system_get_favorites (shell_app_system_get_default ()); - for (iter = favorite_ids; iter; iter = iter->next) - { - const char *id = iter->data; - ShellApp *app; - - app = shell_app_monitor_get_app (monitor, id); - if (app) - apps = g_slist_prepend (apps, g_object_ref (app)); - } - apps = g_slist_reverse (apps); - - return apps; -} - -static gboolean -idle_handle_focus_change (gpointer data) -{ - ShellAppMonitor *monitor = data; - long curtime = get_time (); - - if (monitor->watched_window != NULL) - increment_usage_for_window (monitor, monitor->watched_window); - - monitor->watched_window = get_active_window (monitor); - monitor->watch_start_time = curtime; - - monitor->idle_focus_change_id = 0; - return FALSE; -} - -static void -on_focus_window_changed (MetaDisplay *display, - GParamSpec *spec, - ShellAppMonitor *self) -{ - if (!self->enable_monitoring || self->currently_idle) - return; - - if (self->idle_focus_change_id != 0) - return; - - /* Defensively compress notifications here in case something is going berserk, - * we'll at least use a bit less system resources. */ - self->idle_focus_change_id = g_timeout_add (250, idle_handle_focus_change, self); -} - -static void -ensure_queued_save (ShellAppMonitor *monitor) -{ - if (monitor->save_id != 0) - return; - monitor->save_id = g_timeout_add_seconds (SAVE_APPS_TIMEOUT_SECONDS, idle_save_application_usage, monitor); -} - -/* Used to sort highest scores at the top */ -static gint -usage_sort_apps (gconstpointer data1, - gconstpointer data2) -{ - const AppUsage *u1 = data1; - const AppUsage *u2 = data2; - - if (u1->score > u2->score) - return -1; - else if (u1->score == u2->score) - return 0; - else - return 1; -} - -/* 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 (ShellAppMonitor *monitor) -{ - UsageIterator iter; - const char *context; - const char *id; - AppUsage *usage; - GDate *date; - guint32 date_days; - - /* Subtract a week */ - date = g_date_new (); - g_date_set_time_t (date, time (NULL)); - g_date_subtract_days (date, USAGE_CLEAN_DAYS); - date_days = g_date_get_julian (date); - - usage_iterator_init (monitor, &iter); - - while (usage_iterator_next (monitor, &iter, &context, &id, &usage)) - { - if ((usage->score < SCORE_MIN) && - (usage->last_seen < date_days)) - usage_iterator_remove (monitor, &iter); - } - g_date_free (date); - - 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) -{ - ShellAppMonitor *monitor = SHELL_APP_MONITOR (data); - UsageIterator iter; - const char *current_context; - const char *context; - const char *id; - AppUsage *usage; - GFileOutputStream *output; - GOutputStream *buffered_output; - GDataOutputStream *data_output; - GError *error = NULL; - - monitor->save_id = 0; - - /* Parent directory is already created by shell-global */ - output = g_file_replace (monitor->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; - - usage_iterator_init (monitor, &iter); - - current_context = NULL; - while (usage_iterator_next (monitor, &iter, &context, &id, &usage)) - { - /* Don't save the fake apps we create for windows */ - if (usage->transient) - continue; - - if (context != current_context) - { - if (current_context != NULL) - { - if (!g_data_output_stream_put_string (data_output, " ", NULL, &error)) - goto out; - } - current_context = context; - if (!g_data_output_stream_put_string (data_output, " \n", NULL, &error)) - goto out; - } - if (!g_data_output_stream_put_string (data_output, " window_count > 0, &error)) - goto out; - - if (!write_attribute_double (data_output, "score", usage->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 (current_context != NULL) - { - 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 (G_OUTPUT_STREAM(data_output), NULL, &error); - g_object_unref (data_output); - if (error) - { - g_debug ("Could not save applications usage data: %s", error->message); - g_error_free (error); - } - return FALSE; -} - -typedef struct { - ShellAppMonitor *monitor; - char *context; -} ParseData; - -static void -shell_app_monitor_start_element_handler (GMarkupParseContext *context, - const gchar *element_name, - const gchar **attribute_names, - const gchar **attribute_values, - gpointer user_data, - GError **error) -{ - ParseData *data = user_data; - - if (strcmp (element_name, "application-state") == 0) - { - } - else if (strcmp (element_name, "context") == 0) - { - char *context = NULL; - const char **attribute; - const char **value; - - for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) - { - if (strcmp (*attribute, "id") == 0) - context = g_strdup (*value); - } - if (context < 0) - { - g_set_error (error, - G_MARKUP_ERROR, - G_MARKUP_ERROR_PARSE, - "Missing attribute id on <%s> element", - element_name); - return; - } - data->context = context; - } - else if (strcmp (element_name, "application") == 0) - { - const char **attribute; - const char **value; - AppUsage *usage; - char *appid = NULL; - GHashTable *usage_table; - - for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) - { - if (strcmp (*attribute, "id") == 0) - appid = g_strdup (*value); - } - - if (!appid) - { - g_set_error (error, - G_MARKUP_ERROR, - G_MARKUP_ERROR_PARSE, - "Missing attribute id on <%s> element", - element_name); - return; - } - - usage_table = get_usages_for_context (data->monitor, data->context); - - usage = g_new0 (AppUsage, 1); - usage->initially_seen_sequence = 0; - g_hash_table_insert (usage_table, appid, usage); - - for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) - { - if (strcmp (*attribute, "open-window-count") == 0) - { - guint count = strtoul (*value, NULL, 10); - if (count > 0) - data->monitor->previously_running = g_slist_prepend (data->monitor->previously_running, - usage); - } - else 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 void -shell_app_monitor_end_element_handler (GMarkupParseContext *context, - const gchar *element_name, - gpointer user_data, - GError **error) -{ - ParseData *data = user_data; - - if (strcmp (element_name, "context") == 0) - { - g_free (data->context); - data->context = NULL; - } -} - -static void -shell_app_monitor_text_handler (GMarkupParseContext *context, - const gchar *text, - gsize text_len, - gpointer user_data, - GError **error) -{ - /* do nothing, very very fast */ -} - -static GMarkupParser app_state_parse_funcs = -{ - shell_app_monitor_start_element_handler, - shell_app_monitor_end_element_handler, - shell_app_monitor_text_handler, - NULL, - NULL -}; - -/* Load data about apps usage from file */ -static void -restore_from_file (ShellAppMonitor *monitor) -{ - GFileInputStream *input; - ParseData parse_data; - GMarkupParseContext *parse_context; - GError *error = NULL; - char buf[1024]; - - input = g_file_read (monitor->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; - } - - memset (&parse_data, 0, sizeof (ParseData)); - parse_data.monitor = monitor; - parse_data.context = NULL; - parse_context = g_markup_parse_context_new (&app_state_parse_funcs, 0, &parse_data, 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_free (parse_data.context); - g_markup_parse_context_free (parse_context); - g_input_stream_close ((GInputStream*)input, NULL, NULL); - g_object_unref (input); - - idle_clean_usage (monitor); - monitor->previously_running = g_slist_sort (monitor->previously_running, usage_sort_apps); - - 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 monitoring is disabled, we still - * report apps usage based on (possibly) saved data, but don't collect data. - */ -static void -update_enable_monitoring (ShellAppMonitor *monitor) -{ - GConfValue *value; - gboolean enable; - - value = gconf_client_get (monitor->gconf_client, ENABLE_MONITORING_KEY, NULL); - if (value) - { - enable = gconf_value_get_bool (value); - gconf_value_free (value); - } - else /* Schema is not present, set default value by hand to avoid getting FALSE */ - enable = TRUE; - - /* Be sure not to start the timers if they were already set */ - if (enable && !monitor->enable_monitoring) - { - on_focus_window_changed (NULL, NULL, monitor); - } - /* ...and don't try to stop them if they were not running */ - else if (!enable && monitor->enable_monitoring) - { - monitor->watched_window = NULL; - if (monitor->save_id) - { - g_source_remove (monitor->save_id); - monitor->save_id = 0; - } - } - - monitor->enable_monitoring = enable; -} - -/* Called when the ENABLE_MONITORING_KEY boolean has changed */ -static void -on_enable_monitoring_key_changed (GConfClient *client, - guint connexion_id, - GConfEntry *entry, - gpointer monitor) -{ - update_enable_monitoring ((ShellAppMonitor *) monitor); -} - - -/** - * shell_app_monitor_get_startup_sequences: - * @self: - * - * Returns: (transfer none) (element-type ShellStartupSequence): Currently active startup sequences - */ -GSList * -shell_app_monitor_get_startup_sequences (ShellAppMonitor *self) -{ - ShellGlobal *global = shell_global_get (); - MetaScreen *screen = shell_global_get_screen (global); - return meta_screen_get_startup_sequences (screen); -} - -/* sn_startup_sequence_ref returns void, so make a - * wrapper which returns self */ -static SnStartupSequence * -sequence_ref (SnStartupSequence *sequence) -{ - sn_startup_sequence_ref (sequence); - return sequence; -} - -GType -shell_startup_sequence_get_type (void) -{ - static GType gtype = G_TYPE_INVALID; - if (gtype == G_TYPE_INVALID) - { - gtype = g_boxed_type_register_static ("ShellStartupSequence", - (GBoxedCopyFunc)sequence_ref, - (GBoxedFreeFunc)sn_startup_sequence_unref); - } - return gtype; -} - -const char * -shell_startup_sequence_get_id (ShellStartupSequence *sequence) -{ - return sn_startup_sequence_get_id ((SnStartupSequence*)sequence); -} - -const char * -shell_startup_sequence_get_name (ShellStartupSequence *sequence) -{ - return sn_startup_sequence_get_name ((SnStartupSequence*)sequence); -} - -gboolean -shell_startup_sequence_get_completed (ShellStartupSequence *sequence) -{ - return sn_startup_sequence_get_completed ((SnStartupSequence*)sequence); -} - -/** - * shell_startup_sequence_create_icon: - * @sequence: - * @size: Size in pixels of icon - * - * Returns: (transfer none): A new #ClutterTexture containing an icon for the sequence - */ -ClutterActor * -shell_startup_sequence_create_icon (ShellStartupSequence *sequence, guint size) -{ - GThemedIcon *themed; - const char *icon_name; - ClutterActor *texture; - - icon_name = sn_startup_sequence_get_icon_name ((SnStartupSequence*)sequence); - if (!icon_name) - { - texture = clutter_texture_new (); - clutter_actor_set_size (texture, size, size); - return texture; - } - - themed = (GThemedIcon*)g_themed_icon_new (icon_name); - texture = shell_texture_cache_load_gicon (shell_texture_cache_get_default (), - G_ICON (themed), size); - g_object_unref (G_OBJECT (themed)); - return texture; -} - -/** - * shell_app_monitor_get_default: - * - * Return Value: (transfer none): The global #ShellAppMonitor instance - */ -ShellAppMonitor * -shell_app_monitor_get_default () -{ - static ShellAppMonitor *instance; - - if (instance == NULL) - instance = g_object_new (SHELL_TYPE_APP_MONITOR, NULL); - - return instance; -} diff --git a/src/shell-app-monitor.h b/src/shell-app-monitor.h deleted file mode 100644 index 75b463e0d..000000000 --- a/src/shell-app-monitor.h +++ /dev/null @@ -1,70 +0,0 @@ -/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ -#ifndef __SHELL_APP_MONITOR_H__ -#define __SHELL_APP_MONITOR_H__ - -#include -#include - -#include "window.h" -#include "shell-app.h" -#include "shell-app-system.h" - -/* - * This object provides monitoring of system application directories (.desktop files) - * and activity-based statistics about applications usage - */ - -G_BEGIN_DECLS - -typedef struct _ShellAppMonitor ShellAppMonitor; -typedef struct _ShellAppMonitorClass ShellAppMonitorClass; -typedef struct _ShellAppMonitorPrivate ShellAppMonitorPrivate; - -#define SHELL_TYPE_APP_MONITOR (shell_app_monitor_get_type ()) -#define SHELL_APP_MONITOR(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP_MONITOR, ShellAppMonitor)) -#define SHELL_APP_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass)) -#define SHELL_IS_APP_MONITOR(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_APP_MONITOR)) -#define SHELL_IS_APP_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_APP_MONITOR)) -#define SHELL_APP_MONITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass)) - -struct _ShellAppMonitorClass -{ - GObjectClass parent_class; -}; - -GType shell_app_monitor_get_type (void) G_GNUC_CONST; - -ShellAppMonitor* shell_app_monitor_get_default(void); - -ShellApp *shell_app_monitor_get_window_app (ShellAppMonitor *monitor, MetaWindow *metawin); - -ShellApp *shell_app_monitor_get_app (ShellAppMonitor *monitor, const char *id); - -GSList *shell_app_monitor_get_favorites (ShellAppMonitor *monitor); - -GList *shell_app_monitor_get_most_used_apps (ShellAppMonitor *monitor, - const char *context, - gint number); - -GSList *shell_app_monitor_get_windows_for_app (ShellAppMonitor *monitor, const char *appid); - -gboolean shell_app_monitor_is_window_usage_tracked (MetaWindow *window); - -/* Get whatever's running right now */ -GSList *shell_app_monitor_get_running_apps (ShellAppMonitor *monitor, const char *context); - -GSList *shell_app_monitor_get_startup_sequences (ShellAppMonitor *monitor); - -/* Hidden typedef for SnStartupSequence */ -typedef struct _ShellStartupSequence ShellStartupSequence; -#define SHELL_TYPE_STARTUP_SEQUENCE (shell_startup_sequence_get_type ()) -GType shell_startup_sequence_get_type (void); - -const char *shell_startup_sequence_get_id (ShellStartupSequence *sequence); -const char *shell_startup_sequence_get_name (ShellStartupSequence *sequence); -gboolean shell_startup_sequence_get_completed (ShellStartupSequence *sequence); -ClutterActor *shell_startup_sequence_create_icon (ShellStartupSequence *sequence, guint size); - -G_END_DECLS - -#endif /* __SHELL_APP_MONITOR_H__ */ diff --git a/src/shell-app-private.h b/src/shell-app-private.h new file mode 100644 index 000000000..f3ead0ff9 --- /dev/null +++ b/src/shell-app-private.h @@ -0,0 +1,22 @@ +/* -*- 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 + +ShellAppInfo *_shell_app_get_info (ShellApp *app); + +ShellApp* _shell_app_new_for_window (MetaWindow *window); + +ShellApp* _shell_app_new (ShellAppInfo *appinfo); + +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.c b/src/shell-app-system.c index 610b24a7a..898fd0f95 100644 --- a/src/shell-app-system.c +++ b/src/shell-app-system.c @@ -10,6 +10,7 @@ #include #include +#include "shell-app-private.h" #include "shell-global.h" #include "shell-texture-cache.h" #include "display.h" @@ -33,7 +34,6 @@ enum { enum { INSTALLED_CHANGED, - FAVORITES_CHANGED, LAST_SIGNAL }; @@ -43,6 +43,7 @@ struct _ShellAppSystemPrivate { GMenuTree *apps_tree; GMenuTree *settings_tree; + GHashTable *app_id_to_info; GHashTable *app_id_to_app; GHashTable *cached_menu_contents; /* > */ @@ -50,8 +51,6 @@ struct _ShellAppSystemPrivate { GSList *cached_settings; /* ShellAppInfo */ - GList *cached_favorites; /* utf8 */ - gint app_monitor_id; guint app_change_timeout_id; @@ -62,8 +61,6 @@ static void shell_app_system_finalize (GObject *object); static gboolean on_tree_changed (gpointer user_data); static void on_tree_changed_cb (GMenuTree *tree, gpointer user_data); static void reread_menus (ShellAppSystem *self); -static void on_favorite_apps_changed (GConfClient *client, guint id, GConfEntry *entry, gpointer user_data); -static void reread_favorite_apps (ShellAppSystem *system); G_DEFINE_TYPE(ShellAppSystem, shell_app_system, G_TYPE_OBJECT); @@ -205,14 +202,6 @@ static void shell_app_system_class_init(ShellAppSystemClass *klass) NULL, NULL, g_cclosure_marshal_VOID__VOID, G_TYPE_NONE, 0); - signals[FAVORITES_CHANGED] = - g_signal_new ("favorites-changed", - SHELL_TYPE_APP_SYSTEM, - G_SIGNAL_RUN_LAST, - G_STRUCT_OFFSET (ShellAppSystemClass, favorites_changed), - NULL, NULL, - g_cclosure_marshal_VOID__VOID, - G_TYPE_NONE, 0); g_type_class_add_private (gobject_class, sizeof (ShellAppSystemPrivate)); } @@ -228,8 +217,11 @@ shell_app_system_init (ShellAppSystem *self) ShellAppSystemPrivate); /* The key is owned by the value */ - priv->app_id_to_app = g_hash_table_new_full (g_str_hash, g_str_equal, - NULL, (GDestroyNotify) shell_app_info_unref); + priv->app_id_to_info = g_hash_table_new_full (g_str_hash, g_str_equal, + NULL, (GDestroyNotify) shell_app_info_unref); + + /* Key is owned by info */ + priv->app_id_to_app = g_hash_table_new (g_str_hash, g_str_equal); priv->cached_menu_contents = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, free_appinfo_gslist); @@ -249,10 +241,6 @@ shell_app_system_init (ShellAppSystem *self) reread_menus (self); client = gconf_client_get_default (); - - self->priv->app_monitor_id = gconf_client_notify_add (client, SHELL_APP_FAVORITES_KEY, - on_favorite_apps_changed, self, NULL, NULL); - reread_favorite_apps (self); } static void @@ -269,6 +257,7 @@ shell_app_system_finalize (GObject *object) g_hash_table_destroy (priv->cached_menu_contents); + g_hash_table_destroy (priv->app_id_to_info); g_hash_table_destroy (priv->app_id_to_app); g_slist_foreach (priv->cached_app_menus, (GFunc)shell_app_menu_entry_free, NULL); @@ -279,10 +268,6 @@ shell_app_system_finalize (GObject *object) g_slist_free (priv->cached_settings); priv->cached_settings = NULL; - g_list_free (priv->cached_favorites); - - gconf_client_notify_remove (gconf_client_get_default (), priv->app_monitor_id); - G_OBJECT_CLASS (shell_app_system_parent_class)->finalize(object); } @@ -404,7 +389,7 @@ cache_by_id (ShellAppSystem *self, GSList *apps, gboolean ref) if (ref) shell_app_info_ref (info); /* the name is owned by the info itself */ - g_hash_table_insert (self->priv->app_id_to_app, (char*)shell_app_info_get_id (info), + g_hash_table_insert (self->priv->app_id_to_info, (char*)shell_app_info_get_id (info), info); } } @@ -421,7 +406,7 @@ reread_menus (ShellAppSystem *self) /* Now loop over applications.menu and settings.menu, inserting each by desktop file * ID into a hash */ - g_hash_table_remove_all (self->priv->app_id_to_app); + g_hash_table_remove_all (self->priv->app_id_to_info); trunk = gmenu_tree_get_root_directory (self->priv->apps_tree); apps = gather_entries_recurse (self, NULL, trunk); gmenu_tree_item_unref (trunk); @@ -469,63 +454,6 @@ on_tree_changed_cb (GMenuTree *monitor, gpointer user_data) self, NULL); } -static GList * -convert_gconf_value_string_list_to_list_uniquify (GConfValue *value ) -{ - GSList *list; - GSList *tmp; - GList *result = NULL; - GHashTable *tmp_table = g_hash_table_new (g_str_hash, g_str_equal); - - list = gconf_value_get_list (value); - - for (tmp = list ; tmp; tmp = tmp->next) - { - GConfValue *value = tmp->data; - char *str = g_strdup (gconf_value_get_string (value)); - if (!str) - continue; - if (g_hash_table_lookup (tmp_table, str)) - { - g_free (str); - continue; - } - g_hash_table_insert (tmp_table, str, GUINT_TO_POINTER(1)); - result = g_list_prepend (result, str); - } - g_hash_table_destroy (tmp_table); - return g_list_reverse (result); -} - -static void -reread_favorite_apps (ShellAppSystem *system) -{ - GConfClient *client = gconf_client_get_default (); - GConfValue *val; - - val = gconf_client_get (client, SHELL_APP_FAVORITES_KEY, NULL); - - if (!(val && val->type == GCONF_VALUE_LIST && gconf_value_get_list_type (val) == GCONF_VALUE_STRING)) - return; - - g_list_foreach (system->priv->cached_favorites, (GFunc) g_free, NULL); - g_list_free (system->priv->cached_favorites); - system->priv->cached_favorites = convert_gconf_value_string_list_to_list_uniquify (val); - - gconf_value_free (val); -} - -void -on_favorite_apps_changed (GConfClient *client, - guint id, - GConfEntry *entry, - gpointer user_data) -{ - ShellAppSystem *system = SHELL_APP_SYSTEM (user_data); - reread_favorite_apps (system); - g_signal_emit (G_OBJECT (system), signals[FAVORITES_CHANGED], 0); -} - GType shell_app_info_get_type (void) { @@ -628,93 +556,70 @@ shell_app_system_get_default () return instance; } -/** - * shell_app_system_get_favorites: - * - * Return the list of applications which have been explicitly added to the - * favorites. - * - * Return value: (transfer none) (element-type utf8): List of favorite application ids - */ -GList * -shell_app_system_get_favorites (ShellAppSystem *system) -{ - return system->priv->cached_favorites; -} +typedef struct { + ShellAppSystem *appsys; + ShellAppInfo *info; +} ShellAppRef; static void -set_gconf_value_string_list (GConfValue *val, GList *items) +shell_app_system_on_app_weakref (gpointer data, + GObject *location) { - GList *iter; - GSList *tmp = NULL; + ShellAppRef *ref = data; - for (iter = items; iter; iter = iter->next) - { - const char *str = iter->data; - GConfValue *strval = gconf_value_new (GCONF_VALUE_STRING); - gconf_value_set_string (strval, str); - tmp = g_slist_prepend (tmp, strval); - } - tmp = g_slist_reverse (tmp); - - gconf_value_set_list (val, tmp); - g_slist_free (tmp); -} - -void -shell_app_system_add_favorite (ShellAppSystem *system, const char *id) -{ - GConfClient *client = gconf_client_get_default (); - GConfValue *val; - GList *iter; - - iter = g_list_find_custom (system->priv->cached_favorites, id, (GCompareFunc)strcmp); - if (iter) - return; - - val = gconf_value_new (GCONF_VALUE_LIST); - gconf_value_set_list_type (val, GCONF_VALUE_STRING); - - system->priv->cached_favorites = g_list_append (system->priv->cached_favorites, g_strdup (id)); - - set_gconf_value_string_list (val, system->priv->cached_favorites); - gconf_client_set (client, SHELL_APP_FAVORITES_KEY, val, NULL); -} - -void -shell_app_system_remove_favorite (ShellAppSystem *system, const char *id) -{ - GConfClient *client = gconf_client_get_default (); - GConfValue *val; - GList *iter; - - iter = g_list_find_custom (system->priv->cached_favorites, id, (GCompareFunc)strcmp); - if (!iter) - return; - g_free (iter->data); - system->priv->cached_favorites = g_list_delete_link (system->priv->cached_favorites, iter); - - val = gconf_value_new (GCONF_VALUE_LIST); - gconf_value_set_list_type (val, GCONF_VALUE_STRING); - - set_gconf_value_string_list (val, system->priv->cached_favorites); - gconf_client_set (client, SHELL_APP_FAVORITES_KEY, val, NULL); + g_hash_table_remove (ref->appsys->priv->app_id_to_app, shell_app_info_get_id (ref->info)); + shell_app_info_unref (ref->info); + g_free (ref); } /** - * shell_app_system_lookup_app: + * shell_app_system_get_app: * - * Return value: (transfer full): The #ShellAppInfo for id, or %NULL if none + * Find or create a #ShellApp corresponding to an id; if already cached + * elsewhere in memory, return that instance. Otherwise, create a new + * one. + * + * Return value: (transfer full): The #ShellApp for id, or %NULL if none */ -ShellAppInfo * -shell_app_system_lookup_cached_app (ShellAppSystem *self, const char *id) +ShellApp * +shell_app_system_get_app (ShellAppSystem *self, + const char *id) { ShellAppInfo *info; + ShellApp *app; - info = g_hash_table_lookup (self->priv->app_id_to_app, id); - if (info) - shell_app_info_ref (info); - return info; + app = g_hash_table_lookup (self->priv->app_id_to_app, id); + if (app) + return g_object_ref (app); + + info = g_hash_table_lookup (self->priv->app_id_to_info, id); + if (!info) + return NULL; + + app = _shell_app_new (info); + + return app; +} + +/* ShellAppSystem ensures we have a unique instance of + * apps per id. + */ +void +_shell_app_system_register_app (ShellAppSystem *self, + ShellApp *app) +{ + const char *id; + ShellAppRef *ref; + + id = shell_app_get_id (app); + + g_return_if_fail (g_hash_table_lookup (self->priv->app_id_to_app, id) == NULL); + + ref = g_new0 (ShellAppRef, 1); + ref->appsys = self; + ref->info = shell_app_info_ref (_shell_app_get_info (app)); + g_hash_table_insert (self->priv->app_id_to_app, (char*)shell_app_info_get_id (ref->info), app); + g_object_weak_ref (G_OBJECT (app), shell_app_system_on_app_weakref, ref); } ShellAppInfo * @@ -778,16 +683,16 @@ shell_app_system_create_from_window (ShellAppSystem *system, MetaWindow *window) * heuristically determined application identifier * string, or %NULL if none. * - * Returns: (transfer full): A #ShellAppInfo for name + * Returns: (transfer full): A #ShellApp for name */ -ShellAppInfo * +ShellApp * shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, const char *name) { - ShellAppInfo *result; + ShellApp *result; char **vendor_prefixes; - result = shell_app_system_lookup_cached_app (system, name); + result = shell_app_system_get_app (system, name); if (result != NULL) return result; @@ -795,7 +700,7 @@ shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, *vendor_prefixes; vendor_prefixes++) { char *tmpid = g_strjoin (NULL, *vendor_prefixes, "-", name, NULL); - result = shell_app_system_lookup_cached_app (system, tmpid); + result = shell_app_system_get_app (system, tmpid); g_free (tmpid); if (result != NULL) return result; diff --git a/src/shell-app-system.h b/src/shell-app-system.h index b8d20f9c9..7b8ec482a 100644 --- a/src/shell-app-system.h +++ b/src/shell-app-system.h @@ -4,6 +4,7 @@ #include #include +#include "shell-app.h" #include "window.h" #define SHELL_TYPE_APP_SYSTEM (shell_app_system_get_type ()) @@ -75,9 +76,11 @@ gboolean shell_app_info_launch (ShellAppInfo *info, ShellAppInfo *shell_app_system_load_from_desktop_file (ShellAppSystem *system, const char *filename, GError **error); -ShellAppInfo *shell_app_system_lookup_cached_app (ShellAppSystem *system, const char *id); +ShellApp *shell_app_system_get_app (ShellAppSystem *system, const char *id); -ShellAppInfo *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, const char *id); +void _shell_app_system_register_app (ShellAppSystem *self, ShellApp *app); + +ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, const char *id); ShellAppInfo *shell_app_system_create_from_window (ShellAppSystem *system, MetaWindow *window); @@ -85,10 +88,4 @@ GSList *shell_app_system_get_menus (ShellAppSystem *system); GSList *shell_app_system_get_all_settings (ShellAppSystem *system); -GList *shell_app_system_get_favorites (ShellAppSystem *system); - -void shell_app_system_add_favorite (ShellAppSystem *system, const char *id); - -void shell_app_system_remove_favorite (ShellAppSystem *system, const char *id); - #endif /* __SHELL_APP_SYSTEM_H__ */ diff --git a/src/shell-app-usage.c b/src/shell-app-usage.c new file mode 100644 index 000000000..f02dd6503 --- /dev/null +++ b/src/shell-app-usage.c @@ -0,0 +1,970 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "shell-app-usage.h" +#include "shell-window-tracker.h" +#include "shell-global.h" +#include "shell-marshal.h" + +#include "display.h" +#include "window.h" +#include "group.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 APP_MONITOR_GCONF_DIR SHELL_GCONF_DIR"/app_monitor" +#define ENABLE_MONITORING_KEY APP_MONITOR_GCONF_DIR"/enable_monitoring" + +#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 /* leave this low for testing, we can bump later if need be */ + +/* 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; + DBusGProxy *session_proxy; + GdkDisplay *display; + GConfClient *gconf_client; + gulong last_idle; + guint idle_focus_change_id; + guint save_id; + guint gconf_notify; + gboolean currently_idle; + gboolean enable_monitoring; + + GSList *previously_running; + + long watch_start_time; + ShellApp *watched_app; + + /* > */ + GHashTable *app_usages_for_context; +}; + +G_DEFINE_TYPE (ShellAppUsage, shell_app_usage, G_TYPE_OBJECT); + +/* Represents an application record for a given context */ +struct UsageData +{ + /* Whether the application we're tracking is "transient", see + * shell_app_info_is_transient. + */ + gboolean transient; + + 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 (DBusGProxy *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 UsageData * get_app_usage_for_context_and_id (ShellAppUsage *self, + const char *context, + const char *appid); + +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 (GConfClient *client, + guint connexion_id, + GConfEntry *entry, + gpointer self); + +static long +get_time (void) +{ + GTimeVal tv; + g_get_current_time (&tv); + return tv.tv_sec; +} + +static void +shell_app_usage_class_init (ShellAppUsageClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->finalize = shell_app_usage_finalize; +} + +static GHashTable * +get_usages_for_context (ShellAppUsage *self, + const char *context) +{ + GHashTable *context_usages; + + context_usages = g_hash_table_lookup (self->app_usages_for_context, context); + if (context_usages == NULL) + { + context_usages = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free); + g_hash_table_insert (self->app_usages_for_context, g_strdup (context), + context_usages); + } + return context_usages; +} + +static UsageData * +get_app_usage_for_context_and_id (ShellAppUsage *self, + const char *context, + const char *appid) +{ + UsageData *usage; + GHashTable *context_usages; + + context_usages = get_usages_for_context (self, context); + + usage = g_hash_table_lookup (context_usages, appid); + if (usage) + return usage; + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (context_usages, g_strdup (appid), usage); + + return usage; +} + +static UsageData * +get_usage_for_app (ShellAppUsage *self, + ShellApp *app) +{ + const char *context; + + context = _shell_window_tracker_get_app_context (shell_window_tracker_get_default (), app); + + return get_app_usage_for_context_and_id (self, context, shell_app_get_id (app)); +} + +typedef struct { + gboolean in_context; + GHashTableIter context_iter; + const char *context_id; + GHashTableIter usage_iter; +} UsageIterator; + +static void +usage_iterator_init (ShellAppUsage *self, + UsageIterator *iter) +{ + iter->in_context = FALSE; + g_hash_table_iter_init (&(iter->context_iter), self->app_usages_for_context); +} + +static gboolean +usage_iterator_next (ShellAppUsage *self, + UsageIterator *iter, + const char **context, + const char **id, + UsageData **usage) +{ + gpointer key, value; + gboolean next_context; + + if (!iter->in_context) + next_context = TRUE; + else if (!g_hash_table_iter_next (&(iter->usage_iter), &key, &value)) + next_context = TRUE; + else + next_context = FALSE; + + while (next_context) + { + GHashTable *app_usages; + + if (!g_hash_table_iter_next (&(iter->context_iter), &key, &value)) + return FALSE; + iter->in_context = TRUE; + iter->context_id = key; + app_usages = value; + g_hash_table_iter_init (&(iter->usage_iter), app_usages); + + next_context = !g_hash_table_iter_next (&(iter->usage_iter), &key, &value); + } + + *context = iter->context_id; + *id = key; + *usage = value; + + return TRUE; +} + +static void +usage_iterator_remove (ShellAppUsage *self, + UsageIterator *iter) +{ + g_assert (iter->in_context); + + g_hash_table_iter_remove (&(iter->usage_iter)); +} + +/* Limit the score to a certain level so that most used apps can change */ +static void +normalize_usage (ShellAppUsage *self) +{ + UsageIterator iter; + const char *context; + const char *id; + UsageData *usage; + + usage_iterator_init (self, &iter); + + while (usage_iterator_next (self, &iter, &context, &id, &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_running_changed (ShellWindowTracker *tracker, + ShellApp *app, + gpointer user_data) +{ + ShellAppUsage *self = SHELL_APP_USAGE (user_data); + UsageData *usage; + gboolean running; + + if (shell_app_is_transient (app)) + return; + + usage = get_usage_for_app (self, app); + + running = shell_app_get_n_windows (app) > 0; + + 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 (DBusGProxy *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 +shell_app_usage_init (ShellAppUsage *self) +{ + char *shell_config_dir, *path; + DBusGConnection *session_bus; + ShellWindowTracker *tracker; + + self->app_usages_for_context = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify) g_hash_table_destroy); + + tracker = shell_window_tracker_get_default (); + g_signal_connect (tracker, "notify::focus-app", G_CALLBACK (on_focus_app_changed), self); + g_signal_connect (tracker, "app-running-changed", G_CALLBACK (on_app_running_changed), self); + + session_bus = dbus_g_bus_get (DBUS_BUS_SESSION, NULL); + self->session_proxy = dbus_g_proxy_new_for_name (session_bus, "org.gnome.SessionManager", + "/org/gnome/SessionManager/Presence", + "org.gnome.SessionManager"); + dbus_g_proxy_add_signal (self->session_proxy, "StatusChanged", + G_TYPE_UINT, G_TYPE_INVALID, G_TYPE_INVALID); + dbus_g_proxy_connect_signal (self->session_proxy, "StatusChanged", + G_CALLBACK (on_session_status_changed), self, NULL); + + self->last_idle = 0; + self->currently_idle = FALSE; + self->enable_monitoring = FALSE; + + g_object_get (shell_global_get(), "configdir", &shell_config_dir, NULL), + path = g_build_filename (shell_config_dir, DATA_FILENAME, NULL); + g_free (shell_config_dir); + self->configfile = g_file_new_for_path (path); + g_free (path); + restore_from_file (self); + + self->gconf_client = gconf_client_get_default (); + gconf_client_add_dir (self->gconf_client, APP_MONITOR_GCONF_DIR, + GCONF_CLIENT_PRELOAD_NONE, NULL); + self->gconf_notify = + gconf_client_notify_add (self->gconf_client, ENABLE_MONITORING_KEY, + on_enable_monitoring_key_changed, self, NULL, NULL); + update_enable_monitoring (self); +} + +static void +shell_app_usage_finalize (GObject *object) +{ + ShellAppUsage *self = SHELL_APP_USAGE (object); + + if (self->save_id > 0) + g_source_remove (self->save_id); + gconf_client_notify_remove (self->gconf_client, self->gconf_notify); + g_object_unref (self->gconf_client); + + g_object_unref (self->configfile); + + G_OBJECT_CLASS (shell_app_usage_parent_class)->finalize(object); +} + +typedef struct { + ShellAppUsage *usage; + GHashTable *context_usages; +} SortAppsByUsageData; + +static int +sort_apps_by_usage (gconstpointer a, + gconstpointer b, + gpointer datap) +{ + SortAppsByUsageData *data = 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 (data->context_usages, shell_app_get_id (app_a)); + usage_b = g_hash_table_lookup (data->context_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 + * @context: Activity identifier + * @max_count: how many applications are requested. Note that the actual + * list size may be less, or NULL if not enough applications are registered. + * + * Get a list of most popular applications for a given context. + * + * Returns: (element-type ShellApp) (transfer full): List of applications + */ +GSList * +shell_app_usage_get_most_used (ShellAppUsage *self, + const char *context, + gint max_count) +{ + GSList *apps; + GList *appids, *iter; + GHashTable *usages; + ShellAppSystem *appsys; + SortAppsByUsageData data; + + usages = g_hash_table_lookup (self->app_usages_for_context, context); + if (usages == NULL) + return NULL; + + appsys = shell_app_system_get_default (); + + appids = g_hash_table_get_keys (usages); + apps = NULL; + for (iter = appids; iter; iter = iter->next) + { + const char *appid = iter->data; + ShellApp *app; + + app = shell_app_system_get_app (appsys, appid); + if (!app) + continue; + + apps = g_slist_prepend (apps, g_object_ref (app)); + } + + g_list_free (appids); + + data.usage = self; + data.context_usages = usages; + apps = g_slist_sort_with_data (apps, sort_apps_by_usage, &data); + + return apps; +} + +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); +} + +/* Used to sort highest scores at the top */ +static gint +usage_sort_apps (gconstpointer data1, + gconstpointer data2) +{ + const UsageData *u1 = data1; + const UsageData *u2 = data2; + + if (u1->score > u2->score) + return -1; + else if (u1->score == u2->score) + return 0; + else + return 1; +} + +/* 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) +{ + UsageIterator iter; + const char *context; + const char *id; + UsageData *usage; + long current_time; + long week_ago; + + current_time = get_time (); + week_ago = current_time - (7 * 24 * 60 * 60); + + usage_iterator_init (self, &iter); + + while (usage_iterator_next (self, &iter, &context, &id, &usage)) + { + if ((usage->score < SCORE_MIN) && + (usage->last_seen < week_ago)) + usage_iterator_remove (self, &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); + UsageIterator iter; + const char *current_context; + const char *context; + const char *id; + 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; + + usage_iterator_init (self, &iter); + + current_context = NULL; + while (usage_iterator_next (self, &iter, &context, &id, &usage)) + { + ShellApp *app; + + app = shell_app_system_get_app (shell_app_system_get_default(), id); + + if (!app) + continue; + + if (context != current_context) + { + if (current_context != NULL) + { + if (!g_data_output_stream_put_string (data_output, " ", NULL, &error)) + goto out; + } + current_context = context; + if (!g_data_output_stream_put_string (data_output, " \n", NULL, &error)) + goto out; + } + 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 (current_context != NULL) + { + 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 (G_OUTPUT_STREAM(data_output), NULL, &error); + g_object_unref (data_output); + if (error) + { + g_debug ("Could not save applications usage data: %s", error->message); + g_error_free (error); + } + return FALSE; +} + +typedef struct { + ShellAppUsage *self; + char *context; +} ParseData; + +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) +{ + ParseData *data = user_data; + + if (strcmp (element_name, "application-state") == 0) + { + } + else if (strcmp (element_name, "context") == 0) + { + char *context = NULL; + const char **attribute; + const char **value; + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "id") == 0) + context = g_strdup (*value); + } + if (context < 0) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Missing attribute id on <%s> element", + element_name); + return; + } + data->context = context; + } + else if (strcmp (element_name, "application") == 0) + { + const char **attribute; + const char **value; + UsageData *usage; + char *appid = NULL; + GHashTable *usage_table; + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "id") == 0) + appid = g_strdup (*value); + } + + if (!appid) + { + g_set_error (error, + G_MARKUP_ERROR, + G_MARKUP_ERROR_PARSE, + "Missing attribute id on <%s> element", + element_name); + return; + } + + usage_table = get_usages_for_context (data->self, data->context); + + usage = g_new0 (UsageData, 1); + g_hash_table_insert (usage_table, appid, usage); + + for (attribute = attribute_names, value = attribute_values; *attribute; attribute++, value++) + { + if (strcmp (*attribute, "open-window-count") == 0) + { + guint count = strtoul (*value, NULL, 10); + if (count > 0) + data->self->previously_running = g_slist_prepend (data->self->previously_running, + usage); + } + else 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 void +shell_app_usage_end_element_handler (GMarkupParseContext *context, + const gchar *element_name, + gpointer user_data, + GError **error) +{ + ParseData *data = user_data; + + if (strcmp (element_name, "context") == 0) + { + g_free (data->context); + data->context = NULL; + } +} + +static void +shell_app_usage_text_handler (GMarkupParseContext *context, + const gchar *text, + gsize text_len, + gpointer user_data, + GError **error) +{ + /* do nothing, very very fast */ +} + +static GMarkupParser app_state_parse_funcs = +{ + shell_app_usage_start_element_handler, + shell_app_usage_end_element_handler, + shell_app_usage_text_handler, + NULL, + NULL +}; + +/* Load data about apps usage from file */ +static void +restore_from_file (ShellAppUsage *self) +{ + GFileInputStream *input; + ParseData parse_data; + 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; + } + + memset (&parse_data, 0, sizeof (ParseData)); + parse_data.self = self; + parse_data.context = NULL; + parse_context = g_markup_parse_context_new (&app_state_parse_funcs, 0, &parse_data, 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_free (parse_data.context); + g_markup_parse_context_free (parse_context); + g_input_stream_close ((GInputStream*)input, NULL, NULL); + g_object_unref (input); + + idle_clean_usage (self); + self->previously_running = g_slist_sort (self->previously_running, usage_sort_apps); + + 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) +{ + GConfValue *value; + gboolean enable; + + value = gconf_client_get (self->gconf_client, ENABLE_MONITORING_KEY, NULL); + if (value) + { + enable = gconf_value_get_bool (value); + gconf_value_free (value); + } + else /* Schema is not present, set default value by hand to avoid getting FALSE */ + enable = TRUE; + + /* 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; + if (self->save_id) + { + g_source_remove (self->save_id); + self->save_id = 0; + } + } + + self->enable_monitoring = enable; +} + +/* Called when the ENABLE_MONITORING_KEY boolean has changed */ +static void +on_enable_monitoring_key_changed (GConfClient *client, + guint connexion_id, + GConfEntry *entry, + gpointer self) +{ + update_enable_monitoring ((ShellAppUsage *) self); +} + +/** + * shell_app_usage_get_default: + * + * Return Value: (transfer none): The global #ShellAppUsage instance + */ +ShellAppUsage * +shell_app_usage_get_default () +{ + 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 000000000..f136bdef8 --- /dev/null +++ b/src/shell-app-usage.h @@ -0,0 +1,36 @@ +/* -*- 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 + +typedef struct _ShellAppUsage ShellAppUsage; +typedef struct _ShellAppUsageClass ShellAppUsageClass; +typedef struct _ShellAppUsagePrivate ShellAppUsagePrivate; + +#define SHELL_TYPE_APP_USAGE (shell_app_usage_get_type ()) +#define SHELL_APP_USAGE(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP_USAGE, ShellAppUsage)) +#define SHELL_APP_USAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_APP_USAGE, ShellAppUsageClass)) +#define SHELL_IS_APP_USAGE(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_APP_USAGE)) +#define SHELL_IS_APP_USAGE_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_APP_USAGE)) +#define SHELL_APP_USAGE_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_APP_USAGE, ShellAppUsageClass)) + +struct _ShellAppUsageClass +{ + GObjectClass parent_class; +}; + +GType shell_app_usage_get_type (void) G_GNUC_CONST; + +ShellAppUsage* shell_app_usage_get_default(void); + +GSList *shell_app_usage_get_most_used (ShellAppUsage *monitor, + const char *context, + gint number); + +G_END_DECLS + +#endif /* __SHELL_APP_USAGE_H__ */ diff --git a/src/shell-app.c b/src/shell-app.c index b89fafbec..ac0cf5692 100644 --- a/src/shell-app.c +++ b/src/shell-app.c @@ -1,6 +1,6 @@ /* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ -#include "shell-app.h" +#include "shell-app-private.h" #include "shell-global.h" /** @@ -70,13 +70,20 @@ shell_app_is_transient (ShellApp *app) return shell_app_info_is_transient (app->info); } +gboolean +shell_app_launch (ShellApp *self, + GError **error) +{ + return shell_app_info_launch (self->info, error); +} + /** - * shell_app_get_info: + * _shell_app_get_info: * * Returns: (transfer none): Associated app info */ ShellAppInfo * -shell_app_get_info (ShellApp *app) +_shell_app_get_info (ShellApp *app) { return app->info; } @@ -142,6 +149,12 @@ shell_app_get_windows (ShellApp *app) return app->windows; } +guint +shell_app_get_n_windows (ShellApp *app) +{ + return g_slist_length (app->windows); +} + static gboolean shell_app_has_visible_windows (ShellApp *app) { @@ -219,6 +232,7 @@ _shell_app_new_for_window (MetaWindow *window) app = g_object_new (SHELL_TYPE_APP, NULL); app->info = shell_app_system_create_from_window (shell_app_system_get_default (), window); + _shell_app_system_register_app (shell_app_system_get_default (), app); _shell_app_add_window (app, window); return app; @@ -231,6 +245,7 @@ _shell_app_new (ShellAppInfo *info) app = g_object_new (SHELL_TYPE_APP, NULL); app->info = shell_app_info_ref (info); + _shell_app_system_register_app (shell_app_system_get_default (), app); return app; } diff --git a/src/shell-app.h b/src/shell-app.h index 232b07ad1..f6e2f713e 100644 --- a/src/shell-app.h +++ b/src/shell-app.h @@ -2,11 +2,9 @@ #ifndef __SHELL_APP_H__ #define __SHELL_APP_H__ -#include -#include +#include #include "window.h" -#include "shell-app-system.h" G_BEGIN_DECLS @@ -35,8 +33,9 @@ ClutterActor *shell_app_create_icon_texture (ShellApp *app, float size); char *shell_app_get_name (ShellApp *app); char *shell_app_get_description (ShellApp *app); gboolean shell_app_is_transient (ShellApp *app); +gboolean shell_app_launch (ShellApp *info, GError **error); -ShellAppInfo *shell_app_get_info (ShellApp *app); +guint shell_app_get_n_windows (ShellApp *app); GSList *shell_app_get_windows (ShellApp *app); @@ -44,14 +43,6 @@ gboolean shell_app_is_on_workspace (ShellApp *app, MetaWorkspace *workspace); int shell_app_compare (ShellApp *app, ShellApp *other); -ShellApp* _shell_app_new_for_window (MetaWindow *window); - -ShellApp* _shell_app_new (ShellAppInfo *appinfo); - -void _shell_app_add_window (ShellApp *app, MetaWindow *window); - -void _shell_app_remove_window (ShellApp *app, MetaWindow *window); - G_END_DECLS #endif /* __SHELL_APP_H__ */ diff --git a/src/shell-window-tracker.c b/src/shell-window-tracker.c new file mode 100644 index 000000000..a311a0f9c --- /dev/null +++ b/src/shell-window-tracker.c @@ -0,0 +1,826 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#include +#include + +#include +#include +#include +#include + +#define SN_API_NOT_YET_FROZEN 1 +#include + +#include "shell-window-tracker.h" +#include "shell-app-system.h" +#include "shell-app-private.h" +#include "shell-texture-cache.h" +#include "shell-global.h" +#include "shell-marshal.h" + +#include "display.h" +#include "window.h" +#include "group.h" +#include "util.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-window-tracker + * @short_description: Associate windows with applications + * + * Maintains a mapping from windows to applications (.desktop file ids). + * It currently implements this with some heuristics on the WM_CLASS X11 + * property (and some static override regexps); in the future, we want to + * have it also track through startup-notification. + */ + +/* Title patterns to detect apps that don't set WM class as needed. + * Format: application ID, title regex pattern, NULL (for GRegex) */ +static struct +{ + const char *app_id; + const char *pattern; + GRegex *regex; +} title_patterns[] = { + {"mozilla-firefox.desktop", ".* - Mozilla Firefox", NULL}, \ + {"openoffice.org-writer.desktop", ".* - OpenOffice.org Writer$", NULL}, \ + {"openoffice.org-calc.desktop", ".* - OpenOffice.org Calc$", NULL}, \ + {"openoffice.org-impress.desktop", ".* - OpenOffice.org Impress$", NULL}, \ + {"openoffice.org-draw.desktop", ".* - OpenOffice.org Draw$", NULL}, \ + {"openoffice.org-base.desktop", ".* - OpenOffice.org Base$", NULL}, \ + {"openoffice.org-math.desktop", ".* - OpenOffice.org Math$", NULL}, \ + {NULL, NULL, NULL} +}; + +struct _ShellWindowTracker +{ + GObject parent; + + guint idle_focus_change_id; + ShellApp *focus_app; + + /* */ + GHashTable *window_to_app; + + /* */ + GHashTable *running_apps; +}; + +G_DEFINE_TYPE (ShellWindowTracker, shell_window_tracker, G_TYPE_OBJECT); + +enum { + PROP_0, + PROP_FOCUS_APP +}; + +enum { + APP_RUNNING_CHANGED, + STARTUP_SEQUENCE_CHANGED, + + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +static void shell_window_tracker_finalize (GObject *object); + +static void on_focus_window_changed (MetaDisplay *display, GParamSpec *spec, ShellWindowTracker *tracker); + +static void track_window (ShellWindowTracker *monitor, MetaWindow *window); +static void disassociate_window (ShellWindowTracker *monitor, MetaWindow *window); + + +static void +shell_window_tracker_get_property (GObject *gobject, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + ShellWindowTracker *tracker = SHELL_WINDOW_TRACKER (gobject); + + switch (prop_id) + { + case PROP_FOCUS_APP: + g_value_set_object (value, tracker->focus_app); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec); + break; + } +} + +static void +shell_window_tracker_class_init (ShellWindowTrackerClass *klass) +{ + GObjectClass *gobject_class = G_OBJECT_CLASS (klass); + + gobject_class->get_property = shell_window_tracker_get_property; + gobject_class->finalize = shell_window_tracker_finalize; + + g_object_class_install_property (gobject_class, + PROP_FOCUS_APP, + g_param_spec_object ("focus-app", + "Focus App", + "Focused application", + SHELL_TYPE_APP, + G_PARAM_READABLE)); + + signals[APP_RUNNING_CHANGED] = g_signal_new ("app-running-changed", + SHELL_TYPE_WINDOW_TRACKER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__OBJECT, + G_TYPE_NONE, 1, + SHELL_TYPE_APP); + signals[STARTUP_SEQUENCE_CHANGED] = g_signal_new ("startup-sequence-changed", + SHELL_TYPE_WINDOW_TRACKER, + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, + g_cclosure_marshal_VOID__BOXED, + G_TYPE_NONE, 1, SHELL_TYPE_STARTUP_SEQUENCE); +} + +/** + * get_app_id_from_title: + * + * Use a window's "title" property to determine an application ID. + * This is a temporary crutch for a few applications until we get + * them correctly setting their WM_CLASS. + */ +static const char * +get_app_id_from_title (MetaWindow *window) +{ + static gboolean patterns_initialized = FALSE; + const char *title; + int i; + + title = meta_window_get_title (window); + + if (!patterns_initialized) /* Generate match patterns once for all */ + { + patterns_initialized = TRUE; + for (i = 0; title_patterns[i].app_id; i++) + { + title_patterns[i].regex = g_regex_new (title_patterns[i].pattern, + 0, 0, NULL); + } + } + + /* Match window title patterns to identifiers for non-standard apps */ + if (title) + { + for (i = 0; title_patterns[i].app_id; i++) + { + if (g_regex_match (title_patterns[i].regex, title, 0, NULL)) + { + /* Matched, return the app id we want */ + return title_patterns[i].app_id; + } + } + } + return NULL; +} + +/** + * get_appid_from_window: + * + * Turn the WM_CLASS property into our best guess at a .desktop file id. + */ +static char * +get_appid_from_window (MetaWindow *window) +{ + const char *wmclass; + char *appid_guess; + + wmclass = meta_window_get_wm_class (window); + if (!wmclass) + return NULL; + + appid_guess = g_ascii_strdown (wmclass, -1); + + /* This handles "Fedora Eclipse", probably others. + * Note g_strdelimit is modify-in-place. */ + g_strdelimit (appid_guess, " ", '-'); + + return appid_guess; +} + +/** + * window_is_tracked: + * + * We don't attempt to associate override-redirect windows with applications + * at all, since there's no reason to do so yet. + * + * Returns: %TRUE iff we want to scan this window for application association + */ +static gboolean +window_is_tracked (MetaWindow *window) +{ + if (meta_window_is_override_redirect (window)) + return FALSE; + + return TRUE; +} + +/** + * shell_window_tracker_is_window_interesting: + * + * The ShellWindowTracker associates certain kinds of windows with + * applications; however, others we don't want to + * appear in places where we want to give a list of windows + * for an application, such as the alt-tab dialog. + * + * An example of a window we don't want to show is the root + * desktop window. We skip all override-redirect types, and also + * exclude other window types like tooltip explicitly, though generally + * most of these should be override-redirect. + * + * Returns: %TRUE iff a window is "interesting" + */ +gboolean +shell_window_tracker_is_window_interesting (MetaWindow *window) +{ + if (!window_is_tracked (window)) + return FALSE; + + if (meta_window_is_skip_taskbar (window)) + return FALSE; + + switch (meta_window_get_window_type (window)) + { + /* Definitely ignore these. */ + case META_WINDOW_DESKTOP: + case META_WINDOW_DOCK: + case META_WINDOW_SPLASHSCREEN: + /* Should have already been handled by override_redirect above, + * but explicitly list here so we get the "unhandled enum" + * warning if in the future anything is added.*/ + case META_WINDOW_DROPDOWN_MENU: + case META_WINDOW_POPUP_MENU: + case META_WINDOW_TOOLTIP: + case META_WINDOW_NOTIFICATION: + case META_WINDOW_COMBO: + case META_WINDOW_DND: + case META_WINDOW_OVERRIDE_OTHER: + return FALSE; + case META_WINDOW_NORMAL: + case META_WINDOW_DIALOG: + case META_WINDOW_MODAL_DIALOG: + case META_WINDOW_MENU: + case META_WINDOW_TOOLBAR: + case META_WINDOW_UTILITY: + break; + } + + return TRUE; +} + +/** + * get_app_for_window_direct: + * + * Looks only at the given window, and attempts to determine + * an application based on WM_CLASS. If that fails, then + * a "transient" application is created. + * + * Return value: (transfer full): A newly-referenced #ShellApp + */ +static ShellApp * +get_app_for_window_direct (MetaWindow *window) +{ + ShellApp *app; + ShellAppSystem *appsys; + char *wmclass; + char *with_desktop; + + wmclass = get_appid_from_window (window); + + if (!wmclass) + return _shell_app_new_for_window (window); + + with_desktop = g_strjoin (NULL, wmclass, ".desktop", NULL); + g_free (wmclass); + + appsys = shell_app_system_get_default (); + app = shell_app_system_lookup_heuristic_basename (appsys, with_desktop); + g_free (with_desktop); + + if (app == NULL) + { + const char *id = get_app_id_from_title (window); + + if (id != NULL) + app = shell_app_system_get_app (appsys, id); + } + + return app; +} + +/** + * get_app_for_window: + * + * Determines the application associated with a window, using + * all available information such as the window's MetaGroup, + * and what we know about other windows. + */ +static ShellApp * +get_app_for_window (ShellWindowTracker *monitor, + MetaWindow *window) +{ + ShellApp *result; + MetaWindow *source_window; + GSList *group_windows; + MetaGroup *group; + GSList *iter; + + group = meta_window_get_group (window); + if (group == NULL) + group_windows = g_slist_prepend (NULL, window); + else + group_windows = meta_group_list_windows (group); + + source_window = window; + + result = NULL; + /* Try finding a window in the group of type NORMAL; if we + * succeed, use that as our source. */ + for (iter = group_windows; iter; iter = iter->next) + { + MetaWindow *group_window = iter->data; + + if (meta_window_get_window_type (group_window) != META_WINDOW_NORMAL) + continue; + + source_window = group_window; + result = g_hash_table_lookup (monitor->window_to_app, group_window); + if (result) + break; + } + + g_slist_free (group_windows); + + if (result != NULL) + { + g_object_ref (result); + return result; + } + + return get_app_for_window_direct (source_window); +} + +const char * +_shell_window_tracker_get_app_context (ShellWindowTracker *tracker, ShellApp *app) +{ + return ""; +} + +static MetaWindow * +get_active_window (ShellWindowTracker *monitor) +{ + MetaScreen *screen; + MetaDisplay *display; + MetaWindow *window; + + screen = shell_global_get_screen (shell_global_get ()); + display = meta_screen_get_display (screen); + window = meta_display_get_focus_window (display); + + if (window != NULL && shell_window_tracker_is_window_interesting (window)) + return window; + return NULL; +} + +static void +on_transient_window_title_changed (MetaWindow *window, + GParamSpec *spec, + ShellWindowTracker *self) +{ + ShellAppSystem *appsys; + ShellApp *app; + const char *id; + + /* Check if we now have a mapping using the window title */ + id = get_app_id_from_title (window); + if (id == NULL) + return; + + appsys = shell_app_system_get_default (); + app = shell_app_system_get_app (appsys, id); + if (app == NULL) + return; + g_object_unref (app); + + /* We found an app, don't listen for further title changes */ + g_signal_handlers_disconnect_by_func (window, G_CALLBACK (on_transient_window_title_changed), + self); + + /* It's simplest to just treat this as a remove + add. */ + disassociate_window (self, window); + track_window (self, window); +} + +static void +track_window (ShellWindowTracker *self, + MetaWindow *window) +{ + ShellApp *app; + + if (!window_is_tracked (window)) + return; + + app = get_app_for_window (self, window); + if (!app) + return; + + /* At this point we've stored the association from window -> application */ + g_hash_table_insert (self->window_to_app, window, app); + + /* However, only put interesting windows in the window list for an app. */ + if (!shell_window_tracker_is_window_interesting (window)) + return; + + if (shell_app_is_transient (app)) + { + /* For a transient application, it's possible one of our title regexps + * will match at a later time, i.e. the application may not have set + * its title fully at the time it initially maps a window. Watch + * for title changes and recompute the app. + */ + g_signal_connect (window, "notify::title", G_CALLBACK (on_transient_window_title_changed), self); + } + + _shell_app_add_window (app, window); + + if (shell_app_get_n_windows (app) == 1) + { + /* key is owned by the app */ + g_hash_table_insert (self->running_apps, (char*)shell_app_get_id (app), + app); + g_signal_emit (self, signals[APP_RUNNING_CHANGED], 0, app); + } +} + +static void +shell_window_tracker_on_window_added (MetaWorkspace *workspace, + MetaWindow *window, + gpointer user_data) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); + + track_window (self, window); +} + +static void +disassociate_window (ShellWindowTracker *self, + MetaWindow *window) +{ + ShellApp *app; + + app = g_hash_table_lookup (self->window_to_app, window); + if (!app) + return; + + g_object_ref (app); + + g_hash_table_remove (self->window_to_app, window); + + _shell_app_remove_window (app, window); + + if (shell_app_get_n_windows (app) == 0) + { + const char *id = shell_app_get_id (app); + g_hash_table_remove (self->running_apps, id); + g_signal_emit (self, signals[APP_RUNNING_CHANGED], 0, app); + } + + g_object_unref (app); +} + +static void +shell_window_tracker_on_window_removed (MetaWorkspace *workspace, + MetaWindow *window, + gpointer user_data) +{ + disassociate_window (SHELL_WINDOW_TRACKER (user_data), window); +} + +static void +load_initial_windows (ShellWindowTracker *monitor) +{ + GList *workspaces, *iter; + MetaScreen *screen = shell_global_get_screen (shell_global_get ()); + workspaces = meta_screen_get_workspaces (screen); + + for (iter = workspaces; iter; iter = iter->next) + { + MetaWorkspace *workspace = iter->data; + GList *windows = meta_workspace_list_windows (workspace); + GList *window_iter; + + for (window_iter = windows; window_iter; window_iter = window_iter->next) + { + MetaWindow *window = window_iter->data; + track_window (monitor, window); + } + + g_list_free (windows); + } +} + +static void +shell_window_tracker_on_n_workspaces_changed (MetaScreen *screen, + GParamSpec *pspec, + gpointer user_data) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data); + GList *workspaces, *iter; + + workspaces = meta_screen_get_workspaces (screen); + + for (iter = workspaces; iter; iter = iter->next) + { + MetaWorkspace *workspace = iter->data; + + /* This pair of disconnect/connect is idempotent if we were + * already connected, while ensuring we get connected for + * new workspaces. + */ + g_signal_handlers_disconnect_by_func (workspace, + shell_window_tracker_on_window_added, + self); + g_signal_handlers_disconnect_by_func (workspace, + shell_window_tracker_on_window_removed, + self); + + g_signal_connect (workspace, "window-added", + G_CALLBACK (shell_window_tracker_on_window_added), self); + g_signal_connect (workspace, "window-removed", + G_CALLBACK (shell_window_tracker_on_window_removed), self); + } +} + +static void +init_window_tracking (ShellWindowTracker *self) +{ + MetaDisplay *display; + MetaScreen *screen = shell_global_get_screen (shell_global_get ()); + + g_signal_connect (screen, "notify::n-workspaces", + G_CALLBACK (shell_window_tracker_on_n_workspaces_changed), self); + display = meta_screen_get_display (screen); + g_signal_connect (display, "notify::focus-window", + G_CALLBACK (on_focus_window_changed), self); + + shell_window_tracker_on_n_workspaces_changed (screen, NULL, self); +} + +static void +on_startup_sequence_changed (MetaScreen *screen, + SnStartupSequence *sequence, + ShellWindowTracker *self) +{ + /* Just proxy the signal */ + g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence); +} + +static void +shell_window_tracker_init (ShellWindowTracker *self) +{ + MetaScreen *screen; + + self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal, + NULL, (GDestroyNotify) g_object_unref); + + self->running_apps = g_hash_table_new (g_str_hash, g_str_equal); + + screen = shell_global_get_screen (shell_global_get ()); + + g_signal_connect (G_OBJECT (screen), "startup-sequence-changed", + G_CALLBACK (on_startup_sequence_changed), self); + + load_initial_windows (self); + init_window_tracking (self); +} + +static void +shell_window_tracker_finalize (GObject *object) +{ + ShellWindowTracker *self = SHELL_WINDOW_TRACKER (object); + int i; + + g_hash_table_destroy (self->running_apps); + g_hash_table_destroy (self->window_to_app); + for (i = 0; title_patterns[i].app_id; i++) + g_regex_unref (title_patterns[i].regex); + + G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object); +} + +/** + * shell_window_tracker_get_window_app + * @monitor: An app monitor instance + * @metawin: A #MetaWindow + * + * Returns: (transfer full): Application associated with window + */ +ShellApp * +shell_window_tracker_get_window_app (ShellWindowTracker *monitor, + MetaWindow *metawin) +{ + MetaWindow *transient_for; + ShellApp *app; + + transient_for = meta_window_get_transient_for (metawin); + if (transient_for != NULL) + metawin = transient_for; + + app = g_hash_table_lookup (monitor->window_to_app, metawin); + if (app) + g_object_ref (app); + + return app; +} + +/** + * shell_window_tracker_get_running_apps: + * @monitor: An app monitor instance + * @context: Activity identifier + * + * Returns the set of applications which currently have at least one open + * window in the given context. The returned list will be sorted + * by shell_app_compare(). + * + * Returns: (element-type ShellApp) (transfer full): Active applications + */ +GSList * +shell_window_tracker_get_running_apps (ShellWindowTracker *monitor, + const char *context) +{ + gpointer key, value; + GSList *ret; + GHashTableIter iter; + + g_hash_table_iter_init (&iter, monitor->running_apps); + + ret = NULL; + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellApp *app = value; + + if (strcmp (context, _shell_window_tracker_get_app_context (monitor, app)) != 0) + continue; + + ret = g_slist_prepend (ret, g_object_ref (app)); + } + + ret = g_slist_sort (ret, (GCompareFunc)shell_app_compare); + + return ret; +} + +static gboolean +idle_handle_focus_change (gpointer data) +{ + ShellWindowTracker *tracker = data; + MetaWindow *new_focus_win; + ShellApp *new_focus_app; + + tracker->idle_focus_change_id = 0; + + new_focus_win = get_active_window (tracker); + new_focus_app = new_focus_win ? g_hash_table_lookup (tracker->window_to_app, new_focus_win) : NULL; + + if (new_focus_app == tracker->focus_app) + return FALSE; + + if (tracker->focus_app != NULL) + g_object_unref (tracker->focus_app); + + if (tracker->focus_app != NULL + && (!new_focus_win || !new_focus_app)) + tracker->focus_app = NULL; + else + tracker->focus_app = g_object_ref (new_focus_app); + + g_object_notify (G_OBJECT (tracker), "focus-app"); + + return FALSE; +} + +static void +on_focus_window_changed (MetaDisplay *display, + GParamSpec *spec, + ShellWindowTracker *self) +{ + if (self->idle_focus_change_id != 0) + return; + + self->idle_focus_change_id = meta_later_add (META_LATER_BEFORE_REDRAW, idle_handle_focus_change, self, NULL); +} + + +/** + * shell_window_tracker_get_startup_sequences: + * @self: + * + * Returns: (transfer none) (element-type ShellStartupSequence): Currently active startup sequences + */ +GSList * +shell_window_tracker_get_startup_sequences (ShellWindowTracker *self) +{ + ShellGlobal *global = shell_global_get (); + MetaScreen *screen = shell_global_get_screen (global); + return meta_screen_get_startup_sequences (screen); +} + +/* sn_startup_sequence_ref returns void, so make a + * wrapper which returns self */ +static SnStartupSequence * +sequence_ref (SnStartupSequence *sequence) +{ + sn_startup_sequence_ref (sequence); + return sequence; +} + +GType +shell_startup_sequence_get_type (void) +{ + static GType gtype = G_TYPE_INVALID; + if (gtype == G_TYPE_INVALID) + { + gtype = g_boxed_type_register_static ("ShellStartupSequence", + (GBoxedCopyFunc)sequence_ref, + (GBoxedFreeFunc)sn_startup_sequence_unref); + } + return gtype; +} + +const char * +shell_startup_sequence_get_id (ShellStartupSequence *sequence) +{ + return sn_startup_sequence_get_id ((SnStartupSequence*)sequence); +} + +const char * +shell_startup_sequence_get_name (ShellStartupSequence *sequence) +{ + return sn_startup_sequence_get_name ((SnStartupSequence*)sequence); +} + +gboolean +shell_startup_sequence_get_completed (ShellStartupSequence *sequence) +{ + return sn_startup_sequence_get_completed ((SnStartupSequence*)sequence); +} + +/** + * shell_startup_sequence_create_icon: + * @sequence: + * @size: Size in pixels of icon + * + * Returns: (transfer none): A new #ClutterTexture containing an icon for the sequence + */ +ClutterActor * +shell_startup_sequence_create_icon (ShellStartupSequence *sequence, guint size) +{ + GIcon *themed; + const char *icon_name; + ClutterActor *texture; + + icon_name = sn_startup_sequence_get_icon_name ((SnStartupSequence*)sequence); + if (!icon_name) + { + texture = clutter_texture_new (); + clutter_actor_set_size (texture, size, size); + return texture; + } + + themed = g_themed_icon_new (icon_name); + texture = shell_texture_cache_load_gicon (shell_texture_cache_get_default (), + themed, size); + g_object_unref (G_OBJECT (themed)); + return texture; +} + + +/** + * shell_window_tracker_get_default: + * + * Return Value: (transfer none): The global #ShellWindowTracker instance + */ +ShellWindowTracker * +shell_window_tracker_get_default () +{ + static ShellWindowTracker *instance; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_WINDOW_TRACKER, NULL); + + return instance; +} diff --git a/src/shell-window-tracker.h b/src/shell-window-tracker.h new file mode 100644 index 000000000..a61439e13 --- /dev/null +++ b/src/shell-window-tracker.h @@ -0,0 +1,57 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ +#ifndef __SHELL_WINDOW_TRACKER_H__ +#define __SHELL_WINDOW_TRACKER_H__ + +#include +#include + +#include "window.h" +#include "shell-app.h" +#include "shell-app-system.h" + +G_BEGIN_DECLS + +typedef struct _ShellWindowTracker ShellWindowTracker; +typedef struct _ShellWindowTrackerClass ShellWindowTrackerClass; +typedef struct _ShellWindowTrackerPrivate ShellWindowTrackerPrivate; + +#define SHELL_TYPE_WINDOW_TRACKER (shell_window_tracker_get_type ()) +#define SHELL_WINDOW_TRACKER(object) (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_WINDOW_TRACKER, ShellWindowTracker)) +#define SHELL_WINDOW_TRACKER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_WINDOW_TRACKER, ShellWindowTrackerClass)) +#define SHELL_IS_WINDOW_TRACKER(object) (G_TYPE_CHECK_INSTANCE_TYPE ((object), SHELL_TYPE_WINDOW_TRACKER)) +#define SHELL_IS_WINDOW_TRACKER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_WINDOW_TRACKER)) +#define SHELL_WINDOW_TRACKER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_WINDOW_TRACKER, ShellWindowTrackerClass)) + +struct _ShellWindowTrackerClass +{ + GObjectClass parent_class; +}; + +GType shell_window_tracker_get_type (void) G_GNUC_CONST; + +ShellWindowTracker* shell_window_tracker_get_default(void); + +GSList * shell_window_tracker_get_running_apps (ShellWindowTracker *monitor, + const char *context); + +ShellApp *shell_window_tracker_get_window_app (ShellWindowTracker *monitor, MetaWindow *metawin); + +gboolean shell_window_tracker_is_window_interesting (MetaWindow *window); + +const char *_shell_window_tracker_get_app_context (ShellWindowTracker *tracker, ShellApp *app); + +GSList *shell_window_tracker_get_startup_sequences (ShellWindowTracker *tracker); + +/* Hidden typedef for SnStartupSequence */ +typedef struct _ShellStartupSequence ShellStartupSequence; +#define SHELL_TYPE_STARTUP_SEQUENCE (shell_startup_sequence_get_type ()) +GType shell_startup_sequence_get_type (void); + +const char *shell_startup_sequence_get_id (ShellStartupSequence *sequence); +const char *shell_startup_sequence_get_name (ShellStartupSequence *sequence); +gboolean shell_startup_sequence_get_completed (ShellStartupSequence *sequence); +ClutterActor *shell_startup_sequence_create_icon (ShellStartupSequence *sequence, guint size); + +G_END_DECLS + +#endif /* __SHELL_WINDOW_TRACKER_H__ */