From 10dcc100e9afb82c6e12365478c45785f516abfd Mon Sep 17 00:00:00 2001 From: Colin Walters Date: Thu, 21 Apr 2011 13:35:01 -0400 Subject: [PATCH] Kill off ShellAppInfo, move into ShellApp This dramatically thins down and sanitizes the application code. The ShellAppSystem changes in a number of ways: * Preferences are special cased more explicitly; they aren't apps, they're shortcuts for an app), and we don't have many of them, so don't need e.g. the optimizations in ShellAppSystem for searching. * get_app() changes to lookup_app() and returns null if an app isn't found. The semantics where it tried to find the .desktop file if we didn't know about it were just broken; I am pretty sure no caller needs this, and if they do we'll fix them. * ShellAppSystem maintains two indexes on apps (by desktop file id and by GMenuTreeEntry), but is no longer in the business of dealing with GMenuTree as far as hierarchy and categories go. That is moved up into js/ui/appDisplay.js. Actually, it flattens both apps and settings. Also, ShellWindowTracker is now the sole reference-owner for window-backed apps. We still do the weird "window:0x1234beef" id for these apps, but a reference is not stored in ShellAppSystem. The js/ui/appDisplay.js code is rewritten, and sucks a lot less. Variable names are clearer: _apps -> _appIcons _filterApp -> _visibleApps _filters -> _categoryBox Similarly for function names. We no longer call (for every app) a recursive lookup in GMenuTree to see if it's in a particular section on every category switch; it's all cached. NOTE - this intentionally reverts the incremental loading code from commit 7813c5b93f6bcde8c4beae286e82bfc472b2b656. It's fast enough here without that. https://bugzilla.gnome.org/show_bug.cgi?id=648149 --- js/ui/appDisplay.js | 308 +++--- js/ui/appFavorites.js | 6 +- js/ui/dash.js | 12 +- js/ui/dateMenu.js | 2 +- js/ui/endSessionDialog.js | 2 +- js/ui/lookingGlass.js | 2 +- js/ui/overview.js | 2 +- js/ui/status/accessibility.js | 2 +- js/ui/status/bluetooth.js | 2 +- js/ui/status/keyboard.js | 2 +- js/ui/status/network.js | 2 +- js/ui/status/power.js | 2 +- js/ui/status/volume.js | 2 +- js/ui/statusMenu.js | 4 +- src/Makefile.am | 2 +- src/shell-app-private.h | 11 +- src/shell-app-system.c | 1832 ++++++++++----------------------- src/shell-app-system.h | 68 +- src/shell-app-usage.c | 8 +- src/shell-app.c | 450 +++++++- src/shell-app.h | 19 +- src/shell-window-tracker.c | 28 +- 22 files changed, 1184 insertions(+), 1584 deletions(-) diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js index 811695d06..975667c4b 100644 --- a/js/ui/appDisplay.js +++ b/js/ui/appDisplay.js @@ -3,6 +3,7 @@ const Clutter = imports.gi.Clutter; const GLib = imports.gi.GLib; const Gtk = imports.gi.Gtk; +const GMenu = imports.gi.GMenu; const Shell = imports.gi.Shell; const Lang = imports.lang; const Signals = imports.signals; @@ -35,8 +36,7 @@ AlphabeticalView.prototype = { this._appSystem = Shell.AppSystem.get_default(); this._pendingAppLaterId = 0; - this._apps = []; - this._filterApp = null; + this._appIcons = {}; // desktop file id let box = new St.BoxLayout({ vertical: true }); box.add(this._grid.actor, { y_align: St.Align.START, expand: true }); @@ -63,20 +63,17 @@ AlphabeticalView.prototype = { _removeAll: function() { this._grid.removeAll(); - this._apps = []; + this._appIcons = {}; }, - _addApp: function(appInfo) { - let appIcon = new AppWellIcon(this._appSystem.get_app(appInfo.get_id())); + _addApp: function(app) { + var id = app.get_id(); + let appIcon = new AppWellIcon(app); this._grid.addItem(appIcon.actor); appIcon.actor.connect('key-focus-in', Lang.bind(this, this._ensureIconVisible)); - appIcon._appInfo = appInfo; - if (this._filterApp && !this._filterApp(appInfo)) - appIcon.actor.hide(); - - this._apps.push(appIcon); + this._appIcons[id] = appIcon; }, _ensureIconVisible: function(icon) { @@ -105,52 +102,33 @@ AlphabeticalView.prototype = { transition: 'easeOutQuad' }); }, - setFilter: function(filter) { - this._filterApp = filter; - for (let i = 0; i < this._apps.length; i++) - this._apps[i].actor.visible = filter(this._apps[i]._appInfo); - }, - - // Create actors for the applications in an idle to avoid blocking - // for too long; see bug 647778 - _addPendingApps: function() { - let i; - let startTimeMillis = new Date().getTime(); - for (i = 0; i < this._pendingAppIds.length; i++) { - let id = this._pendingAppIds[i]; - this._addApp(this._pendingApps[id]); - - let currentTimeMillis = new Date().getTime(); - if (currentTimeMillis - startTimeMillis > MAX_APPLICATION_WORK_MILLIS) - break; - } - this._pendingAppIds.splice(0, i + 1); - if (this._pendingAppIds.length > 0) { - return true; + setVisibleApps: function(apps) { + if (apps == null) { // null implies "all" + for (var id in this._appIcons) { + var icon = this._appIcons[id]; + icon.actor.visible = true; + } } else { - this._pendingAppLaterId = 0; - this._pendingAppIds = null; - this._pendingApps = null; - return false; + // Set everything to not-visible, then set to visible what we should see + for (var id in this._appIcons) { + var icon = this._appIcons[id]; + icon.actor.visible = false; + } + for (var i = 0; i < apps.length; i++) { + var app = apps[i]; + var id = app.get_id(); + var icon = this._appIcons[id]; + icon.actor.visible = true; + } } }, - refresh: function(apps) { - let ids = []; - for (let i in apps) - ids.push(i); - ids.sort(function(a, b) { - return apps[a].get_name().localeCompare(apps[b].get_name()); - }); - + setAppList: function(apps) { this._removeAll(); - - this._pendingAppIds = ids; - this._pendingApps = apps; - if (this._pendingAppLaterId) - Meta.later_remove(this._pendingAppLaterId); - this._pendingAppLaterId = Meta.later_add(Meta.LaterType.BEFORE_REDRAW, - Lang.bind(this, this._addPendingApps)); + for (var i = 0; i < apps.length; i++) { + var app = apps[i]; + this._addApp(app); + } } }; @@ -171,23 +149,24 @@ ViewByCategories.prototype = { // -2 is a flag to indicate that nothing is selected // (used only before the actor is mapped the first time) this._currentCategory = -2; - this._filters = new St.BoxLayout({ vertical: true, reactive: true }); - this._filtersBox = new St.ScrollView({ x_fill: false, - y_fill: false, - style_class: 'vfade' }); - this._filtersBox.add_actor(this._filters); + this._categories = []; + this._apps = null; + + this._categoryBox = new St.BoxLayout({ vertical: true, reactive: true }); + this._categoryScroll = new St.ScrollView({ x_fill: false, + y_fill: false, + style_class: 'vfade' }); + this._categoryScroll.add_actor(this._categoryBox); this.actor.add(this._view.actor, { expand: true, x_fill: true, y_fill: true }); - this.actor.add(this._filtersBox, { expand: false, y_fill: false, y_align: St.Align.START }); + this.actor.add(this._categoryScroll, { expand: false, y_fill: false, y_align: St.Align.START }); // Always select the "All" filter when switching to the app view this.actor.connect('notify::mapped', Lang.bind(this, function() { - if (this.actor.mapped && this._allFilter) + if (this.actor.mapped && this._allCategoryButton) this._selectCategory(-1); })); - this._sections = []; - // We need a dummy actor to catch the keyboard focus if the // user Ctrl-Alt-Tabs here before the deferred work creates // our real contents @@ -201,64 +180,94 @@ ViewByCategories.prototype = { this._currentCategory = num; - if (num != -1) - this._allFilter.remove_style_pseudo_class('selected'); - else - this._allFilter.add_style_pseudo_class('selected'); + if (num != -1) { + var category = this._categories[num]; + this._allCategoryButton.remove_style_pseudo_class('selected'); + this._view.setVisibleApps(category.apps); + } else { + this._allCategoryButton.add_style_pseudo_class('selected'); + this._view.setVisibleApps(null); + } - this._view.setFilter(Lang.bind(this, function(app) { - if (num == -1) - return true; - return this._sections[num].name == app.get_section(); - })); - - for (let i = 0; i < this._sections.length; i++) { + for (var i = 0; i < this._categories.length; i++) { if (i == num) - this._sections[i].filterActor.add_style_pseudo_class('selected'); + this._categories[i].button.add_style_pseudo_class('selected'); else - this._sections[i].filterActor.remove_style_pseudo_class('selected'); + this._categories[i].button.remove_style_pseudo_class('selected'); } }, - _addFilter: function(name, num) { + // Recursively load a GMenuTreeDirectory; we could put this in ShellAppSystem too + _loadCategory: function(dir, appList) { + var iter = dir.iter(); + var nextType; + while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) { + if (nextType == GMenu.TreeItemType.ENTRY) { + var entry = iter.get_entry(); + var app = this._appSystem.lookup_app_by_tree_entry(entry); + appList.push(app); + } else if (nextType == GMenu.TreeItemType.DIRECTORY) { + _loadCategory(iter.get_directory(), appList); + } + } + }, + + _addCategory: function(name, index, dir, allApps) { let button = new St.Button({ label: GLib.markup_escape_text (name, -1), style_class: 'app-filter', x_align: St.Align.START, can_focus: true }); - this._filters.add(button, { expand: true, x_fill: true, y_fill: false }); button.connect('clicked', Lang.bind(this, function() { - this._selectCategory(num); + this._selectCategory(index); })); - if (num != -1) - this._sections[num] = { filterActor: button, - name: name }; - else - this._allFilter = button; + var apps; + if (dir == null) { + apps = allApps; + this._allCategoryButton = button; + } else { + apps = []; + this._loadCategory(dir, apps); + this._categories.push({ apps: apps, + name: name, + button: button }); + } + + this._categoryBox.add(button, { expand: true, x_fill: true, y_fill: false }); }, _removeAll: function() { - this._sections = []; - this._filters.destroy_children(); + this._categories = []; + this._categoryBox.destroy_children(); }, - refresh: function(apps) { + refresh: function() { this._removeAll(); - let sections = this._appSystem.get_sections(); - this._apps = apps; + var allApps = Shell.AppSystem.get_default().get_all(); + allApps.sort(function(a, b) { + return a.compare_by_name(b); + }); /* Translators: Filter to display all applications */ - this._addFilter(_("All"), -1); + this._addCategory(_("All"), -1, null, allApps); - if (!sections) - return; + var tree = this._appSystem.get_tree(); + var root = tree.get_root_directory(); - for (let i = 0; i < sections.length; i++) - this._addFilter(sections[i], i); + var iter = root.iter(); + var nextType; + var i = 0; + while ((nextType = iter.next()) != GMenu.TreeItemType.INVALID) { + if (nextType == GMenu.TreeItemType.DIRECTORY) { + var dir = iter.get_directory(); + this._addCategory(dir.get_name(), i, dir); + i++; + } + } + this._view.setAppList(allApps); this._selectCategory(-1); - this._view.refresh(apps); if (this._focusDummy) { let focused = this._focusDummy.has_key_focus(); @@ -291,31 +300,24 @@ AllAppDisplay.prototype = { }, _redisplay: function() { - let apps = this._appSystem.get_flattened_apps().filter(function(app) { - return !app.get_is_nodisplay(); - }); - - this._appView.refresh(apps); + this._appView.refresh(); } }; -function BaseAppSearchProvider() { +function AppSearchProvider() { this._init(); } -BaseAppSearchProvider.prototype = { +AppSearchProvider.prototype = { __proto__: Search.SearchProvider.prototype, - _init: function(name) { - Search.SearchProvider.prototype._init.call(this, name); + _init: function() { + Search.SearchProvider.prototype._init.call(this, _("APPLICATIONS")); this._appSys = Shell.AppSystem.get_default(); }, - getResultMeta: function(resultId) { - let app = this._appSys.get_app(resultId); - if (!app) - return null; - return { 'id': resultId, + getResultMeta: function(app) { + return { 'id': app, 'name': app.get_name(), 'createIcon': function(size) { return app.create_icon_texture(size); @@ -323,6 +325,14 @@ BaseAppSearchProvider.prototype = { }; }, + getInitialResultSet: function(terms) { + return this._appSys.initial_search(terms); + }, + + getSubsearchResultSet: function(previousResults, terms) { + return this._appSys.subsearch(previousResults, terms); + }, + activateResult: function(id, params) { params = Params.parse(params, { workspace: null, timestamp: null }); @@ -332,7 +342,7 @@ BaseAppSearchProvider.prototype = { let modifiers = event ? Shell.get_event_state(event) : 0; let openNewWindow = modifiers & Clutter.ModifierType.CONTROL_MASK; - let app = this._appSys.get_app(id); + let app = this._appSys.lookup_app(id); if (openNewWindow) app.open_new_window(workspace); else @@ -343,54 +353,62 @@ BaseAppSearchProvider.prototype = { params = Params.parse(params, { workspace: null, timestamp: null }); - let app = this._appSys.get_app(id); + let app = this._appSys.lookup_app(id); app.open_new_window(params.workspace ? params.workspace.index() : -1); - } -}; - -function AppSearchProvider() { - this._init(); -} - -AppSearchProvider.prototype = { - __proto__: BaseAppSearchProvider.prototype, - - _init: function() { - BaseAppSearchProvider.prototype._init.call(this, _("APPLICATIONS")); - }, - - getInitialResultSet: function(terms) { - return this._appSys.initial_search(false, terms); - }, - - getSubsearchResultSet: function(previousResults, terms) { - return this._appSys.subsearch(false, previousResults, terms); }, createResultActor: function (resultMeta, terms) { - let app = this._appSys.get_app(resultMeta['id']); + let app = resultMeta['id']; let icon = new AppWellIcon(app); return icon.actor; } }; -function PrefsSearchProvider() { +function SettingsSearchProvider() { this._init(); } -PrefsSearchProvider.prototype = { - __proto__: BaseAppSearchProvider.prototype, +SettingsSearchProvider.prototype = { + __proto__: Search.SearchProvider.prototype, _init: function() { - BaseAppSearchProvider.prototype._init.call(this, _("SETTINGS")); + Search.SearchProvider.prototype._init.call(this, _("SETTINGS")); + this._appSys = Shell.AppSystem.get_default(); + this._gnomecc = this._appSys.lookup_app('gnome-control-center.desktop'); + }, + + getResultMeta: function(pref) { + return { 'id': pref, + 'name': pref.get_name(), + 'createIcon': function(size) { + return pref.create_icon_texture(size); + } + }; }, getInitialResultSet: function(terms) { - return this._appSys.initial_search(true, terms); + return this._appSys.search_settings(terms); }, getSubsearchResultSet: function(previousResults, terms) { - return this._appSys.subsearch(true, previousResults, terms); + return this._appSys.search_settings(terms); + }, + + activateResult: function(pref, params) { + params = Params.parse(params, { workspace: null, + timestamp: null }); + + pref.activate(params.workspace); + }, + + dragActivateResult: function(pref, params) { + this.activateResult(pref, params); + }, + + createResultActor: function (resultMeta, terms) { + let app = resultMeta['id']; + let icon = new AppWellIcon(app); + return icon.actor; } }; @@ -416,12 +434,12 @@ AppIcon.prototype = { } }; -function AppWellIcon(app, iconParams) { - this._init(app, iconParams); +function AppWellIcon(app, iconParams, onActivateOverride) { + this._init(app, iconParams, onActivateOverride); } AppWellIcon.prototype = { - _init : function(app, iconParams) { + _init : function(app, iconParams, onActivateOverride) { this.app = app; this.actor = new St.Button({ style_class: 'app-well-app', reactive: true, @@ -436,6 +454,8 @@ AppWellIcon.prototype = { this.actor.label_actor = this.icon.label; + // A function callback to override the default "app.activate()"; used by preferences + this._onActivateOverride = onActivateOverride; this.actor.connect('button-press-event', Lang.bind(this, this._onButtonPress)); this.actor.connect('clicked', Lang.bind(this, this._onClicked)); this.actor.connect('popup-menu', Lang.bind(this, this._onKeyboardPopupMenu)); @@ -569,11 +589,15 @@ AppWellIcon.prototype = { this.emit('launching'); let modifiers = Shell.get_event_state(event); - if (modifiers & Clutter.ModifierType.CONTROL_MASK - && this.app.state == Shell.AppState.RUNNING) { - this.app.open_new_window(-1); + if (this._onActivateOverride) { + this._onActivateOverride(event); } else { - this.app.activate(-1); + if (modifiers & Clutter.ModifierType.CONTROL_MASK + && this.app.state == Shell.AppState.RUNNING) { + this.app.open_new_window(-1); + } else { + this.app.activate(-1); + } } Main.overview.hide(); }, diff --git a/js/ui/appFavorites.js b/js/ui/appFavorites.js index a5dce274f..d4f74553e 100644 --- a/js/ui/appFavorites.js +++ b/js/ui/appFavorites.js @@ -28,7 +28,7 @@ AppFavorites.prototype = { let ids = global.settings.get_strv(this.FAVORITE_APPS_KEY); let appSys = Shell.AppSystem.get_default(); let apps = ids.map(function (id) { - return appSys.get_app(id); + return appSys.lookup_app(id); }).filter(function (app) { return app != null; }); @@ -65,7 +65,7 @@ AppFavorites.prototype = { if (appId in this._favorites) return false; - let app = Shell.AppSystem.get_default().get_app(appId); + let app = Shell.AppSystem.get_default().lookup_app(appId); if (!app) return false; @@ -84,7 +84,7 @@ AppFavorites.prototype = { if (!this._addFavorite(appId, pos)) return; - let app = Shell.AppSystem.get_default().get_app(appId); + let app = Shell.AppSystem.get_default().lookup_app(appId); Main.overview.shellInfo.setMessage(_("%s has been added to your favorites.").format(app.get_name()), Lang.bind(this, function () { this._removeFavorite(appId); diff --git a/js/ui/dash.js b/js/ui/dash.js index 2b3d080d0..f2c652c61 100644 --- a/js/ui/dash.js +++ b/js/ui/dash.js @@ -207,7 +207,7 @@ RemoveFavoriteIcon.prototype = { let app = null; if (source instanceof AppDisplay.AppWellIcon) { let appSystem = Shell.AppSystem.get_default(); - app = appSystem.get_app(source.getId()); + app = appSystem.lookup_app(source.getId()); } else if (source.metaWindow) { let tracker = Shell.WindowTracker.get_default(); app = tracker.get_window_app(source.metaWindow); @@ -330,7 +330,7 @@ Dash.prototype = { _onDragMotion: function(dragEvent) { let app = null; if (dragEvent.source instanceof AppDisplay.AppWellIcon) - app = this._appSystem.get_app(dragEvent.source.getId()); + app = this._appSystem.lookup_app(dragEvent.source.getId()); else if (dragEvent.source.metaWindow) app = this._tracker.get_window_app(dragEvent.source.metaWindow); else @@ -619,12 +619,12 @@ Dash.prototype = { handleDragOver : function(source, actor, x, y, time) { let app = null; if (source instanceof AppDisplay.AppWellIcon) - app = this._appSystem.get_app(source.getId()); + app = this._appSystem.lookup_app(source.getId()); else if (source.metaWindow) app = this._tracker.get_window_app(source.metaWindow); // Don't allow favoriting of transient apps - if (app == null || app.is_transient()) + if (app == null || app.is_window_backed()) return DND.DragMotionResult.NO_DROP; let favorites = AppFavorites.getAppFavorites().getFavorites(); @@ -704,13 +704,13 @@ Dash.prototype = { acceptDrop : function(source, actor, x, y, time) { let app = null; if (source instanceof AppDisplay.AppWellIcon) { - app = this._appSystem.get_app(source.getId()); + app = this._appSystem.lookup_app(source.getId()); } else if (source.metaWindow) { app = this._tracker.get_window_app(source.metaWindow); } // Don't allow favoriting of transient apps - if (app == null || app.is_transient()) { + if (app == null || app.is_window_backed()) { return false; } diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js index a6a519633..99ad76d4a 100644 --- a/js/ui/dateMenu.js +++ b/js/ui/dateMenu.js @@ -199,7 +199,7 @@ DateMenuButton.prototype = { _onPreferencesActivate: function() { this.menu.close(); Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-datetime-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-datetime-panel.desktop'); app.activate(-1); }, diff --git a/js/ui/endSessionDialog.js b/js/ui/endSessionDialog.js index b4e458d1f..cad887d78 100644 --- a/js/ui/endSessionDialog.js +++ b/js/ui/endSessionDialog.js @@ -113,7 +113,7 @@ function findAppFromInhibitor(inhibitor) { let app = null; for (let i = 0; i < candidateDesktopFiles.length; i++) { try { - app = appSystem.get_app(candidateDesktopFiles[i]); + app = appSystem.lookup_app(candidateDesktopFiles[i]); if (app) break; diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js index bafcb3721..03830c2e7 100644 --- a/js/ui/lookingGlass.js +++ b/js/ui/lookingGlass.js @@ -247,7 +247,7 @@ WindowList.prototype = { box.add(propsBox); propsBox.add(new St.Label({ text: 'wmclass: ' + metaWindow.get_wm_class() })); let app = tracker.get_window_app(metaWindow); - if (app != null && !app.is_transient()) { + if (app != null && !app.is_window_backed()) { let icon = app.create_icon_texture(22); let propBox = new St.BoxLayout({ style: 'spacing: 6px; ' }); propsBox.add(propBox); diff --git a/js/ui/overview.js b/js/ui/overview.js index 085f29224..8f33efa86 100644 --- a/js/ui/overview.js +++ b/js/ui/overview.js @@ -188,7 +188,7 @@ Overview.prototype = { // Default search providers this.viewSelector.addSearchProvider(new AppDisplay.AppSearchProvider()); - this.viewSelector.addSearchProvider(new AppDisplay.PrefsSearchProvider()); + this.viewSelector.addSearchProvider(new AppDisplay.SettingsSearchProvider()); this.viewSelector.addSearchProvider(new PlaceDisplay.PlaceSearchProvider()); this.viewSelector.addSearchProvider(new DocDisplay.DocSearchProvider()); diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js index 560042c27..542c230b7 100644 --- a/js/ui/status/accessibility.js +++ b/js/ui/status/accessibility.js @@ -90,7 +90,7 @@ ATIndicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Universal Access Settings"), function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-universal-access-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-universal-access-panel.desktop'); app.activate(-1); }); }, diff --git a/js/ui/status/bluetooth.js b/js/ui/status/bluetooth.js index 4cf8d3be5..a73eb7e83 100644 --- a/js/ui/status/bluetooth.js +++ b/js/ui/status/bluetooth.js @@ -91,7 +91,7 @@ Indicator.prototype = { this.menu.addAction(_("Bluetooth Settings"), function() { Main.overview.hide() - let app = Shell.AppSystem.get_default().get_app('bluetooth-properties.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('bluetooth-properties.desktop'); app.activate(-1); }); diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js index 5c332334d..0ae99299b 100644 --- a/js/ui/status/keyboard.js +++ b/js/ui/status/keyboard.js @@ -74,7 +74,7 @@ XKBIndicator.prototype = { })); this.menu.addAction(_("Region and Language Settings"), function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-region-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-region-panel.desktop'); app.activate(-1); }); }, diff --git a/js/ui/status/network.js b/js/ui/status/network.js index a4081ffee..38b2e25ab 100644 --- a/js/ui/status/network.js +++ b/js/ui/status/network.js @@ -1604,7 +1604,7 @@ NMApplet.prototype = { this.menu.addAction(_("Network Settings"), function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-network-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-network-panel.desktop'); app.activate(-1); }); diff --git a/js/ui/status/power.js b/js/ui/status/power.js index a64a2ba6a..1f5b5bb3c 100644 --- a/js/ui/status/power.js +++ b/js/ui/status/power.js @@ -82,7 +82,7 @@ Indicator.prototype = { this.menu.addAction(_("Power Settings"),function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-power-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-power-panel.desktop'); app.activate(-1); }); diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js index bd74cd9d5..adc120791 100644 --- a/js/ui/status/volume.js +++ b/js/ui/status/volume.js @@ -63,7 +63,7 @@ Indicator.prototype = { this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); this.menu.addAction(_("Sound Settings"), function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-sound-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-sound-panel.desktop'); app.activate(-1); }); diff --git a/js/ui/statusMenu.js b/js/ui/statusMenu.js index c506be0dd..f2d825c49 100644 --- a/js/ui/statusMenu.js +++ b/js/ui/statusMenu.js @@ -274,13 +274,13 @@ StatusMenuButton.prototype = { _onMyAccountActivate: function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-user-accounts-panel.desktop'); + let app = Shell.AppSystem.get_default().lookup_setting('gnome-user-accounts-panel.desktop'); app.activate(-1); }, _onPreferencesActivate: function() { Main.overview.hide(); - let app = Shell.AppSystem.get_default().get_app('gnome-control-center.desktop'); + let app = Shell.AppSystem.get_default().lookup_app('gnome-control-center.desktop'); app.activate(-1); }, diff --git a/src/Makefile.am b/src/Makefile.am index cb84d6ee8..b20e03d75 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -269,7 +269,7 @@ libgnome_shell_la_LIBADD = \ libgnome_shell_la_CPPFLAGS = $(gnome_shell_cflags) Shell-0.1.gir: libgnome-shell.la St-1.0.gir -Shell_0_1_gir_INCLUDES = Clutter-1.0 ClutterX11-1.0 Meta-3.0 TelepathyGLib-0.12 TelepathyLogger-0.2 Soup-2.4 +Shell_0_1_gir_INCLUDES = Clutter-1.0 ClutterX11-1.0 Meta-3.0 TelepathyGLib-0.12 TelepathyLogger-0.2 Soup-2.4 GMenu-3.0 Shell_0_1_gir_CFLAGS = $(libgnome_shell_la_CPPFLAGS) -I $(srcdir) Shell_0_1_gir_LIBS = libgnome-shell.la Shell_0_1_gir_FILES = $(libgnome_shell_la_gir_sources) diff --git a/src/shell-app-private.h b/src/shell-app-private.h index 052dfbc9c..a5bf198e5 100644 --- a/src/shell-app-private.h +++ b/src/shell-app-private.h @@ -10,11 +10,9 @@ G_BEGIN_DECLS -ShellAppInfo *_shell_app_get_info (ShellApp *app); - ShellApp* _shell_app_new_for_window (MetaWindow *window); -ShellApp* _shell_app_new (ShellAppInfo *appinfo); +ShellApp* _shell_app_new (GMenuTreeEntry *entry); void _shell_app_handle_startup_sequence (ShellApp *app, SnStartupSequence *sequence); @@ -22,6 +20,13 @@ void _shell_app_add_window (ShellApp *app, MetaWindow *window); void _shell_app_remove_window (ShellApp *app, MetaWindow *window); +void _shell_app_do_match (ShellApp *app, + GSList *terms, + GSList **multiple_prefix_results, + GSList **prefix_results, + GSList **multiple_substring_results, + GSList **substring_results); + G_END_DECLS #endif /* __SHELL_APP_PRIVATE_H__ */ diff --git a/src/shell-app-system.c b/src/shell-app-system.c index 48b0ddfff..9808a2b69 100644 --- a/src/shell-app-system.c +++ b/src/shell-app-system.c @@ -17,9 +17,6 @@ #include "shell-global.h" #include "st.h" -#define GMENU_I_KNOW_THIS_IS_UNSTABLE -#include - /* Vendor prefixes are something that can be preprended to a .desktop * file name. Undo this. */ @@ -42,126 +39,21 @@ static guint signals[LAST_SIGNAL] = { 0 }; struct _ShellAppSystemPrivate { GMenuTree *apps_tree; - GMenuTree *settings_tree; - GHashTable *app_id_to_info; - GHashTable *app_id_to_app; + GHashTable *entry_to_app; - GSList *cached_flattened_apps; /* ShellAppInfo */ - GSList *cached_settings; /* ShellAppInfo */ GSList *known_vendor_prefixes; - gboolean loaded; - gint app_monitor_id; - - guint app_change_timeout_id; + GMenuTree *settings_tree; + GHashTable *setting_entry_to_app; }; -static char *shell_app_info_get_prefix (ShellAppInfo *info); 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_apps_tree_changed_cb (GMenuTree *tree, gpointer user_data); +static void on_settings_tree_changed_cb (GMenuTree *tree, gpointer user_data); G_DEFINE_TYPE(ShellAppSystem, shell_app_system, G_TYPE_OBJECT); -typedef enum { - SHELL_APP_INFO_TYPE_ENTRY, - SHELL_APP_INFO_TYPE_DESKTOP_FILE, - SHELL_APP_INFO_TYPE_WINDOW -} ShellAppInfoType; - -struct _ShellAppInfo { - ShellAppInfoType type; - - /* We need this for two reasons. First, GKeyFile doesn't have a refcount. - * http://bugzilla.gnome.org/show_bug.cgi?id=590808 - * - * But more generally we'll always need it so we know when to free this - * structure (short of weak references on each item). - */ - guint refcount; - - char *casefolded_name; - char *name_collation_key; - char *casefolded_description; - char *casefolded_exec; - - GMenuTreeEntry *entry; - - GKeyFile *keyfile; - char *keyfile_path; - - MetaWindow *window; - char *window_id; -}; - -ShellAppInfo* -shell_app_info_ref (ShellAppInfo *info) -{ - info->refcount++; - return info; -} - -void -shell_app_info_unref (ShellAppInfo *info) -{ - if (--info->refcount > 0) - return; - - g_free (info->casefolded_name); - g_free (info->name_collation_key); - g_free (info->casefolded_description); - - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - gmenu_tree_item_unref (info->entry); - break; - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - g_key_file_free (info->keyfile); - g_free (info->keyfile_path); - break; - case SHELL_APP_INFO_TYPE_WINDOW: - g_object_unref (info->window); - g_free (info->window_id); - break; - } - g_slice_free (ShellAppInfo, info); -} - -static ShellAppInfo * -shell_app_info_new_from_tree_item (GMenuTreeEntry *entry) -{ - ShellAppInfo *info; - - if (!entry) - return NULL; - - info = g_slice_alloc0 (sizeof (ShellAppInfo)); - info->type = SHELL_APP_INFO_TYPE_ENTRY; - info->refcount = 1; - info->entry = (GMenuTreeEntry*)gmenu_tree_item_ref (entry); - return info; -} - -static ShellAppInfo * -shell_app_info_new_from_window (MetaWindow *window) -{ - ShellAppInfo *info; - - info = g_slice_alloc0 (sizeof (ShellAppInfo)); - info->type = SHELL_APP_INFO_TYPE_WINDOW; - info->refcount = 1; - info->window = g_object_ref (window); - /* For windows, its id is simply its pointer address as a string. - * There are various other alternatives, but the address is unique - * and unchanging, which is pretty much the best we can do. - */ - info->window_id = g_strdup_printf ("window:%p", window); - return info; -} - static void shell_app_system_class_init(ShellAppSystemClass *klass) { GObjectClass *gobject_class = (GObjectClass *)klass; @@ -189,26 +81,25 @@ shell_app_system_init (ShellAppSystem *self) SHELL_TYPE_APP_SYSTEM, ShellAppSystemPrivate); - /* The key is owned by the value */ - 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->entry_to_app = g_hash_table_new_full (NULL, NULL, + (GDestroyNotify)gmenu_tree_item_unref, + (GDestroyNotify)g_object_unref); + priv->setting_entry_to_app = g_hash_table_new_full (NULL, NULL, + (GDestroyNotify)gmenu_tree_item_unref, + (GDestroyNotify)g_object_unref); /* For now, we want to pick up Evince, Nautilus, etc. We'll * handle NODISPLAY semantics at a higher level or investigate them * case by case. */ priv->apps_tree = gmenu_tree_new ("applications.menu", GMENU_TREE_FLAGS_INCLUDE_NODISPLAY); - priv->settings_tree = gmenu_tree_new ("gnomecc.menu", GMENU_TREE_FLAGS_INCLUDE_NODISPLAY); + g_signal_connect (priv->apps_tree, "changed", G_CALLBACK (on_apps_tree_changed_cb), self); - priv->app_change_timeout_id = 0; + priv->settings_tree = gmenu_tree_new ("gnomecc.menu", 0); + g_signal_connect (priv->settings_tree, "changed", G_CALLBACK (on_settings_tree_changed_cb), self); - g_signal_connect (priv->apps_tree, "changed", G_CALLBACK (on_tree_changed_cb), self); - g_signal_connect (priv->settings_tree, "changed", G_CALLBACK (on_tree_changed_cb), self); - - reread_menus (self); + on_apps_tree_changed_cb (priv->apps_tree, self); + on_settings_tree_changed_cb (priv->settings_tree, self); } static void @@ -220,771 +111,18 @@ shell_app_system_finalize (GObject *object) g_object_unref (priv->apps_tree); g_object_unref (priv->settings_tree); - g_hash_table_destroy (priv->app_id_to_info); - g_hash_table_destroy (priv->app_id_to_app); - - g_slist_foreach (priv->cached_flattened_apps, (GFunc)shell_app_info_unref, NULL); - g_slist_free (priv->cached_flattened_apps); - priv->cached_flattened_apps = NULL; + g_hash_table_destroy (priv->entry_to_app); + g_hash_table_destroy (priv->setting_entry_to_app); g_slist_foreach (priv->known_vendor_prefixes, (GFunc)g_free, NULL); g_slist_free (priv->known_vendor_prefixes); priv->known_vendor_prefixes = NULL; - g_slist_foreach (priv->cached_settings, (GFunc)shell_app_info_unref, NULL); - g_slist_free (priv->cached_settings); - priv->cached_settings = NULL; - - G_OBJECT_CLASS (shell_app_system_parent_class)->finalize(object); -} - -static GSList * -gather_entries_recurse (ShellAppSystem *monitor, - GSList *apps, - GHashTable *unique, - GMenuTreeDirectory *root) -{ - GMenuTreeIter *iter = gmenu_tree_directory_iter (root); - GMenuTreeItemType next_type; - - while ((next_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID) - { - gpointer item = NULL; - - switch (next_type) - { - case GMENU_TREE_ITEM_INVALID: - break; - case GMENU_TREE_ITEM_ENTRY: - { - ShellAppInfo *app; - item = gmenu_tree_iter_get_entry (iter); - app = shell_app_info_new_from_tree_item (item); - if (!g_hash_table_lookup (unique, shell_app_info_get_id (app))) - { - apps = g_slist_prepend (apps, app); - g_hash_table_insert (unique, (char*)shell_app_info_get_id (app), app); - } - } - break; - case GMENU_TREE_ITEM_DIRECTORY: - { - item = gmenu_tree_iter_get_directory (iter); - apps = gather_entries_recurse (monitor, apps, unique, (GMenuTreeDirectory*)item); - } - break; - default: - break; - } - gmenu_tree_item_unref (item); - } - - gmenu_tree_iter_unref (iter); - - return apps; -} - -static void -reread_entries (ShellAppSystem *self, - GSList **cache, - GHashTable *unique, - GMenuTree *tree) -{ - GMenuTreeDirectory *trunk; - - trunk = gmenu_tree_get_root_directory (tree); - - g_slist_foreach (*cache, (GFunc)shell_app_info_unref, NULL); - g_slist_free (*cache); - *cache = NULL; - - if (!trunk) - { - *cache = NULL; - } - else - { - *cache = gather_entries_recurse (self, *cache, unique, trunk); - gmenu_tree_item_unref (trunk); - } -} - -static void -cache_by_id (ShellAppSystem *self, GSList *apps) -{ - GSList *iter; - - for (iter = apps; iter; iter = iter->next) - { - ShellAppInfo *info = iter->data; - const char *id = shell_app_info_get_id (info); - char *prefix = shell_app_info_get_prefix (info); - - shell_app_info_ref (info); - /* the name is owned by the info itself */ - - if (prefix - && !g_slist_find_custom (self->priv->known_vendor_prefixes, prefix, - (GCompareFunc)g_strcmp0)) - self->priv->known_vendor_prefixes = g_slist_append (self->priv->known_vendor_prefixes, - prefix); - else - g_free (prefix); - g_hash_table_replace (self->priv->app_id_to_info, (char*)id, info); - } -} - -static void -reread_menus (ShellAppSystem *self) -{ - GHashTable *unique; - GError *error = NULL; - - if (!self->priv->loaded) - { - if (!gmenu_tree_load_sync (self->priv->apps_tree, &error)) - { - g_warning ("Failed to load apps: %s", error->message); - return; - } - if (!gmenu_tree_load_sync (self->priv->settings_tree, &error)) - { - g_warning ("Failed to load settings: %s", error->message); - return; - } - self->priv->loaded = TRUE; - } - - unique = g_hash_table_new (g_str_hash, g_str_equal); - - g_slist_foreach (self->priv->known_vendor_prefixes, (GFunc)g_free, NULL); - g_slist_free (self->priv->known_vendor_prefixes); - self->priv->known_vendor_prefixes = NULL; - - reread_entries (self, &(self->priv->cached_flattened_apps), unique, self->priv->apps_tree); - g_hash_table_remove_all (unique); - reread_entries (self, &(self->priv->cached_settings), unique, self->priv->settings_tree); - g_hash_table_destroy (unique); - - g_hash_table_remove_all (self->priv->app_id_to_info); - - cache_by_id (self, self->priv->cached_flattened_apps); - cache_by_id (self, self->priv->cached_settings); -} - -static gboolean -on_tree_changed (gpointer user_data) -{ - ShellAppSystem *self = SHELL_APP_SYSTEM (user_data); - - self->priv->loaded = FALSE; - reread_menus (self); - - g_signal_emit (self, signals[INSTALLED_CHANGED], 0); - - self->priv->app_change_timeout_id = 0; - return FALSE; -} - -static void -on_tree_changed_cb (GMenuTree *monitor, gpointer user_data) -{ - ShellAppSystem *self = SHELL_APP_SYSTEM (user_data); - - /* GMenu currently gives us a separate notification on the entire - * menu tree for each node in the tree that might potentially have - * changed. (See http://bugzilla.gnome.org/show_bug.cgi?id=172046.) - * We need to compress these to avoid doing large extra amounts of - * work. - * - * Even when that bug is fixed, compression is still useful; for one - * thing we want to need to compress across notifications of changes - * to the settings tree. Second we want to compress if multiple - * changes are made to the desktop files at different times but in - * short succession. - */ - - if (self->priv->app_change_timeout_id != 0) - return; - self->priv->app_change_timeout_id = g_timeout_add_full (G_PRIORITY_DEFAULT_IDLE, 3000, - (GSourceFunc) on_tree_changed, - self, NULL); -} - -GType -shell_app_info_get_type (void) -{ - static GType gtype = G_TYPE_INVALID; - if (gtype == G_TYPE_INVALID) - { - gtype = g_boxed_type_register_static ("ShellAppInfo", - (GBoxedCopyFunc)shell_app_info_ref, - (GBoxedFreeFunc)shell_app_info_unref); - } - return gtype; -} - -/** - * shell_app_system_get_flattened_apps: - * - * Traverses a toplevel menu, and returns all items under it. Nested items - * are flattened. This value is computed on initial call and cached thereafter - * until the set of installed applications changes. - * - * Return value: (transfer none) (element-type ShellAppInfo): List of applications - */ -GSList * -shell_app_system_get_flattened_apps (ShellAppSystem *self) -{ - return self->priv->cached_flattened_apps; -} - -/** - * shell_app_system_get_all_settings: - * - * Returns a list of application items under "settings.menu". - * - * Return value: (transfer none) (element-type ShellAppInfo): List of applications - */ -GSList * -shell_app_system_get_all_settings (ShellAppSystem *monitor) -{ - return monitor->priv->cached_settings; -} - -/** - * shell_app_system_get_default: - * - * Return Value: (transfer none): The global #ShellAppSystem singleton - */ -ShellAppSystem * -shell_app_system_get_default () -{ - static ShellAppSystem *instance = NULL; - - if (instance == NULL) - instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL); - - return instance; -} - -typedef struct { - ShellAppSystem *appsys; - ShellAppInfo *info; -} ShellAppRef; - -static void -shell_app_system_on_app_weakref (gpointer data, - GObject *location) -{ - ShellAppRef *ref = data; - - 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_get_app: - * - * 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 - */ -ShellApp * -shell_app_system_get_app (ShellAppSystem *self, - const char *id) -{ - ShellAppInfo *info; - ShellApp *app; - - 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; -} - -/** - * shell_app_system_get_app_for_path: - * @system: a #ShellAppSystem - * @desktop_path: (type utf8): UTF-8 encoded absolute file name - * - * Find or create a #ShellApp corresponding to a given absolute - * file name which must be in the standard paths (XDG_DATA_DIRS). - * For files outside the datadirs, this function returns %NULL. - * - * 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 - */ -ShellApp * -shell_app_system_get_app_for_path (ShellAppSystem *system, - const char *desktop_path) -{ - const char *basename; - ShellAppInfo *info; - - basename = g_strrstr (desktop_path, "/"); - if (basename) - basename += 1; - else - basename = desktop_path; - - info = g_hash_table_lookup (system->priv->app_id_to_info, basename); - if (!info) - return NULL; - - if (info->type == SHELL_APP_INFO_TYPE_ENTRY) - { - const char *full_path = gmenu_tree_entry_get_desktop_file_path (info->entry); - if (strcmp (desktop_path, full_path) != 0) - return NULL; - } - else - return NULL; - - return shell_app_system_get_app (system, basename); -} - -/** - * shell_app_system_get_app_for_window: - * @self: A #ShellAppSystem - * @window: A #MetaWindow - * - * Find or create a #ShellApp for window - * - * Return value: (transfer full): The #ShellApp for window, or %NULL if none - */ -ShellApp * -shell_app_system_get_app_for_window (ShellAppSystem *self, - MetaWindow *window) -{ - char *id = g_strdup_printf ("window:%p", window); - ShellApp *app = g_hash_table_lookup (self->priv->app_id_to_app, id); - - if (app) - g_object_ref (G_OBJECT (app)); - else - app = _shell_app_new_for_window (window); - - g_free (id); - - 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); -} - -/** - * shell_app_system_create_from_window: - * - * In the case where we can't otherwise determine an application - * associated with a window, this function can create a "fake" - * application just backed by information from the window itself. - * - * Return value: (transfer full): A new #ShellAppInfo - */ -ShellAppInfo * -shell_app_system_create_from_window (ShellAppSystem *system, MetaWindow *window) -{ - return shell_app_info_new_from_window (window); -} - -/** - * shell_app_system_lookup_heuristic_basename: - * @system: a #ShellAppSystem - * @id: Probable application identifier - * - * Find a valid application corresponding to a given - * heuristically determined application identifier - * string, or %NULL if none. - * - * Returns: (transfer full): A #ShellApp for @name - */ -ShellApp * -shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, - const char *name) -{ - ShellApp *result; - GSList *prefix; - result = shell_app_system_get_app (system, name); - if (result != NULL) - return result; - for (prefix = system->priv->known_vendor_prefixes; prefix; prefix = g_slist_next (prefix)) - { - char *tmpid = g_strconcat ((char*)prefix->data, name, NULL); - result = shell_app_system_get_app (system, tmpid); - g_free (tmpid); - if (result != NULL) - return result; - } - - return NULL; -} - -typedef enum { - MATCH_NONE, - MATCH_SUBSTRING, /* Not prefix, substring */ - MATCH_MULTIPLE_SUBSTRING, /* Matches multiple criteria with substrings */ - MATCH_PREFIX, /* Strict prefix */ - MATCH_MULTIPLE_PREFIX, /* Matches multiple criteria, at least one prefix */ -} ShellAppInfoSearchMatch; - -static char * -normalize_and_casefold (const char *str) -{ - char *normalized, *result; - - if (str == NULL) - return NULL; - - normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL); - result = g_utf8_casefold (normalized, -1); - g_free (normalized); - return result; + G_OBJECT_CLASS (shell_app_system_parent_class)->finalize (object); } static char * -trim_exec_line (const char *str) -{ - const char *start, *end, *pos; - - end = strchr (str, ' '); - if (end == NULL) - end = str + strlen (str); - - start = str; - while ((pos = strchr (start, '/')) && pos < end) - start = ++pos; - - return g_strndup (start, end - start); -} - -static void -shell_app_info_init_search_data (ShellAppInfo *info) -{ - const char *name; - const char *exec; - const char *comment; - char *normalized_exec; - GDesktopAppInfo *appinfo; - - g_assert (info->type == SHELL_APP_INFO_TYPE_ENTRY); - - appinfo = gmenu_tree_entry_get_app_info (info->entry); - name = g_app_info_get_name (G_APP_INFO (appinfo)); - info->casefolded_name = normalize_and_casefold (name); - - comment = g_app_info_get_description (G_APP_INFO (appinfo)); - info->casefolded_description = normalize_and_casefold (comment); - - exec = g_app_info_get_executable (G_APP_INFO (appinfo)); - normalized_exec = normalize_and_casefold (exec); - info->casefolded_exec = trim_exec_line (normalized_exec); - g_free (normalized_exec); -} - -static ShellAppInfoSearchMatch -shell_app_info_match_terms (ShellAppInfo *info, - GSList *terms) -{ - GSList *iter; - ShellAppInfoSearchMatch match; - - if (G_UNLIKELY(!info->casefolded_name)) - shell_app_info_init_search_data (info); - - match = MATCH_NONE; - for (iter = terms; iter; iter = iter->next) - { - ShellAppInfoSearchMatch current_match; - const char *term = iter->data; - const char *p; - - current_match = MATCH_NONE; - - p = strstr (info->casefolded_name, term); - if (p == info->casefolded_name) - current_match = MATCH_PREFIX; - else if (p != NULL) - current_match = MATCH_SUBSTRING; - - p = strstr (info->casefolded_exec, term); - if (p != NULL) - { - if (p == info->casefolded_exec) - current_match = (current_match == MATCH_NONE) ? MATCH_PREFIX - : MATCH_MULTIPLE_PREFIX; - else if (current_match < MATCH_PREFIX) - current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING - : MATCH_MULTIPLE_SUBSTRING; - } - - if (info->casefolded_description && current_match < MATCH_PREFIX) - { - /* Only do substring matches, as prefix matches are not meaningful - * enough for descriptions - */ - p = strstr (info->casefolded_description, term); - if (p != NULL) - current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING - : MATCH_MULTIPLE_SUBSTRING; - } - - if (current_match == MATCH_NONE) - return current_match; - - if (current_match > match) - match = current_match; - } - return match; -} - -static gint -shell_app_info_compare (gconstpointer a, - gconstpointer b, - gpointer data) -{ - ShellAppSystem *system = data; - const char *id_a = a; - const char *id_b = b; - ShellAppInfo *info_a = g_hash_table_lookup (system->priv->app_id_to_info, id_a); - ShellAppInfo *info_b = g_hash_table_lookup (system->priv->app_id_to_info, id_b); - GDesktopAppInfo *app_info_a = gmenu_tree_entry_get_app_info (info_a->entry); - GDesktopAppInfo *app_info_b = gmenu_tree_entry_get_app_info (info_b->entry); - - if (!info_a->name_collation_key) - info_a->name_collation_key = g_utf8_collate_key (g_app_info_get_name ((GAppInfo*)app_info_a), -1); - if (!info_b->name_collation_key) - info_b->name_collation_key = g_utf8_collate_key (g_app_info_get_name ((GAppInfo*)app_info_b), -1); - - return strcmp (info_a->name_collation_key, info_b->name_collation_key); -} - -static GSList * -sort_and_concat_results (ShellAppSystem *system, - GSList *multiple_prefix_matches, - GSList *prefix_matches, - GSList *multiple_substring_matches, - GSList *substring_matches) -{ - multiple_prefix_matches = g_slist_sort_with_data (multiple_prefix_matches, - shell_app_info_compare, - system); - prefix_matches = g_slist_sort_with_data (prefix_matches, - shell_app_info_compare, - system); - multiple_substring_matches = g_slist_sort_with_data (multiple_substring_matches, - shell_app_info_compare, - system); - substring_matches = g_slist_sort_with_data (substring_matches, - shell_app_info_compare, - system); - return g_slist_concat (multiple_prefix_matches, g_slist_concat (prefix_matches, g_slist_concat (multiple_substring_matches, substring_matches))); -} - -/** - * normalize_terms: - * @terms: (element-type utf8): Input search terms - * - * Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms - */ -static GSList * -normalize_terms (GSList *terms) -{ - GSList *normalized_terms = NULL; - GSList *iter; - for (iter = terms; iter; iter = iter->next) - { - const char *term = iter->data; - normalized_terms = g_slist_prepend (normalized_terms, normalize_and_casefold (term)); - } - return normalized_terms; -} - -static inline void -shell_app_system_do_match (ShellAppSystem *system, - ShellAppInfo *info, - GSList *terms, - GSList **multiple_prefix_results, - GSList **prefix_results, - GSList **multiple_substring_results, - GSList **substring_results) -{ - const char *id = shell_app_info_get_id (info); - ShellAppInfoSearchMatch match; - - if (shell_app_info_get_is_nodisplay (info)) - return; - - match = shell_app_info_match_terms (info, terms); - switch (match) - { - case MATCH_NONE: - break; - case MATCH_MULTIPLE_PREFIX: - *multiple_prefix_results = g_slist_prepend (*multiple_prefix_results, - (char *) id); - break; - case MATCH_PREFIX: - *prefix_results = g_slist_prepend (*prefix_results, (char *) id); - break; - case MATCH_MULTIPLE_SUBSTRING: - *multiple_substring_results = g_slist_prepend (*multiple_substring_results, - (char *) id); - break; - case MATCH_SUBSTRING: - *substring_results = g_slist_prepend (*substring_results, (char *) id); - break; - } -} - -static GSList * -shell_app_system_initial_search_internal (ShellAppSystem *self, - GSList *terms, - GSList *source) -{ - GSList *multiple_prefix_results = NULL; - GSList *prefix_results = NULL; - GSList *multiple_subtring_results = NULL; - GSList *substring_results = NULL; - GSList *iter; - GSList *normalized_terms = normalize_terms (terms); - - for (iter = source; iter; iter = iter->next) - { - ShellAppInfo *info = iter->data; - - shell_app_system_do_match (self, info, normalized_terms, - &multiple_prefix_results, &prefix_results, - &multiple_subtring_results, &substring_results); - } - g_slist_foreach (normalized_terms, (GFunc)g_free, NULL); - g_slist_free (normalized_terms); - - return sort_and_concat_results (self, multiple_prefix_results, prefix_results, multiple_subtring_results, substring_results); -} - -/** - * shell_app_system_initial_search: - * @system: A #ShellAppSystem - * @prefs: %TRUE if we should search preferences instead of apps - * @terms: (element-type utf8): List of terms, logical AND - * - * Search through applications for the given search terms. Note that returned - * strings are only valid until a return to the main loop. - * - * Returns: (transfer container) (element-type utf8): List of application identifiers - */ -GSList * -shell_app_system_initial_search (ShellAppSystem *self, - gboolean prefs, - GSList *terms) -{ - return shell_app_system_initial_search_internal (self, terms, - prefs ? self->priv->cached_settings : self->priv->cached_flattened_apps); -} - -/** - * shell_app_system_subsearch: - * @system: A #ShellAppSystem - * @prefs: %TRUE if we should search preferences instead of apps - * @previous_results: (element-type utf8): List of previous results - * @terms: (element-type utf8): List of terms, logical AND - * - * Search through a previous result set; for more information, see - * js/ui/search.js. Note the value of @prefs must be - * the same as passed to shell_app_system_initial_search(). Note that returned - * strings are only valid until a return to the main loop. - * - * Returns: (transfer container) (element-type utf8): List of application identifiers - */ -GSList * -shell_app_system_subsearch (ShellAppSystem *system, - gboolean prefs, - GSList *previous_results, - GSList *terms) -{ - GSList *iter; - GSList *multiple_prefix_results = NULL; - GSList *prefix_results = NULL; - GSList *multiple_substring_results = NULL; - GSList *substring_results = NULL; - GSList *normalized_terms = normalize_terms (terms); - - /* Note prefs is deliberately ignored; both apps and prefs are in app_id_to_app, - * but we have the parameter for consistency and in case in the future - * they're not in the same data structure. - */ - - for (iter = previous_results; iter; iter = iter->next) - { - const char *id = iter->data; - ShellAppInfo *info; - - info = g_hash_table_lookup (system->priv->app_id_to_info, id); - if (!info) - continue; - - shell_app_system_do_match (system, info, normalized_terms, - &multiple_prefix_results, &prefix_results, - &multiple_substring_results, &substring_results); - } - g_slist_foreach (normalized_terms, (GFunc)g_free, NULL); - g_slist_free (normalized_terms); - - /* Note that a shorter term might have matched as a prefix, but - when extended only as a substring, so we have to redo the - sort rather than reusing the existing ordering */ - return sort_and_concat_results (system, multiple_prefix_results, prefix_results, multiple_substring_results, substring_results); -} - -const char * -shell_app_info_get_id (ShellAppInfo *info) -{ - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - return gmenu_tree_entry_get_desktop_file_id (info->entry); - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - return info->keyfile_path; - case SHELL_APP_INFO_TYPE_WINDOW: - return info->window_id; - } - g_assert_not_reached (); - return NULL; -} - -static char * -shell_app_info_get_prefix (ShellAppInfo *info) +get_prefix_for_entry (GMenuTreeEntry *entry) { char *prefix = NULL, *file_prefix = NULL; const char *id; @@ -992,11 +130,8 @@ shell_app_info_get_prefix (ShellAppInfo *info) char *name; int i = 0; - if (info->type != SHELL_APP_INFO_TYPE_ENTRY) - return NULL; - - id = gmenu_tree_entry_get_desktop_file_id (info->entry); - file = g_file_new_for_path (gmenu_tree_entry_get_desktop_file_path (info->entry)); + id = gmenu_tree_entry_get_desktop_file_id (entry); + file = g_file_new_for_path (gmenu_tree_entry_get_desktop_file_path (entry)); name = g_file_get_basename (file); if (!name) @@ -1092,441 +227,534 @@ shell_app_info_get_prefix (ShellAppInfo *info) g_return_val_if_reached (NULL); } -#define DESKTOP_ENTRY_GROUP "Desktop Entry" - -char * -shell_app_info_get_name (ShellAppInfo *info) +static void +load_app_entry (ShellAppSystem *self, + GMenuTreeEntry *entry) { - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - { - const char *name = g_app_info_get_name (G_APP_INFO (gmenu_tree_entry_get_app_info (info->entry))); - return g_strdup (name); - } - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - return g_key_file_get_locale_string (info->keyfile, DESKTOP_ENTRY_GROUP, "Name", NULL, NULL); - case SHELL_APP_INFO_TYPE_WINDOW: - { - const char *name; + char *prefix; + ShellApp *app; - name = meta_window_get_wm_class (info->window); - if (!name) - name = _("Unknown"); - return g_strdup (name); - } - } - g_assert_not_reached (); - return NULL; -} + if (g_hash_table_lookup (self->priv->entry_to_app, entry)) + return; -char * -shell_app_info_get_description (ShellAppInfo *info) -{ - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - { - const char *description = g_app_info_get_description (G_APP_INFO (gmenu_tree_entry_get_app_info (info->entry))); - return g_strdup (description); - } - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - return g_key_file_get_locale_string (info->keyfile, DESKTOP_ENTRY_GROUP, "Comment", NULL, NULL); - case SHELL_APP_INFO_TYPE_WINDOW: - return NULL; - } - g_assert_not_reached (); - return NULL; -} + prefix = get_prefix_for_entry (entry); -char * -shell_app_info_get_executable (ShellAppInfo *info) -{ - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - { - const char *exec = g_app_info_get_executable (G_APP_INFO (gmenu_tree_entry_get_app_info (info->entry))); - return g_strdup (exec); - } - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - return g_key_file_get_string (info->keyfile, DESKTOP_ENTRY_GROUP, "Exec", NULL); - case SHELL_APP_INFO_TYPE_WINDOW: - return NULL; - } - g_assert_not_reached (); - return NULL; -} - -char * -shell_app_info_get_desktop_file_path (ShellAppInfo *info) -{ - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - return g_strdup (gmenu_tree_entry_get_desktop_file_path (info->entry)); - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - return g_strdup (info->keyfile_path); - case SHELL_APP_INFO_TYPE_WINDOW: - return NULL; - } - g_assert_not_reached (); - return NULL; -} - -static GIcon * -themed_icon_from_name (const char *iconname) -{ - GIcon *icon; - - if (!iconname) - return NULL; - - if (g_path_is_absolute (iconname)) - { - GFile *file; - file = g_file_new_for_path (iconname); - icon = G_ICON (g_file_icon_new (file)); - g_object_unref (file); - } + if (prefix + && !g_slist_find_custom (self->priv->known_vendor_prefixes, prefix, + (GCompareFunc)g_strcmp0)) + self->priv->known_vendor_prefixes = g_slist_append (self->priv->known_vendor_prefixes, + prefix); else - { - char *tmp_name, *p; - tmp_name = strdup (iconname); - /* Work around a common mistake in desktop files */ - if ((p = strrchr (tmp_name, '.')) != NULL && - (strcmp (p, ".png") == 0 || - strcmp (p, ".xpm") == 0 || - strcmp (p, ".svg") == 0)) - { - *p = 0; - } - icon = g_themed_icon_new (tmp_name); - g_free (tmp_name); - } + g_free (prefix); - return icon; -} + app = _shell_app_new (entry); -/** - * shell_app_info_get_icon: - * @info: A #ShellAppInfo - * - * Get the #GIcon associated with this app; for apps "faked" from a #MetaWindow, - * return %NULL. - * - * Returns: (transfer full): The icon for @info, or %NULL - */ -GIcon * -shell_app_info_get_icon (ShellAppInfo *info) -{ - char *iconname = NULL; - GIcon *icon; - - /* This code adapted from gdesktopappinfo.c - * Copyright (C) 2006-2007 Red Hat, Inc. - * Copyright © 2007 Ryan Lortie - * LGPL - */ - - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - return g_object_ref (g_app_info_get_icon (G_APP_INFO (gmenu_tree_entry_get_app_info (info->entry)))); - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - iconname = g_key_file_get_locale_string (info->keyfile, DESKTOP_ENTRY_GROUP, "Icon", NULL, NULL); - icon = themed_icon_from_name (iconname); - g_free (iconname); - return icon; - break; - case SHELL_APP_INFO_TYPE_WINDOW: - return NULL; - } - g_assert_not_reached (); - return NULL; -} - -/** - * shell_app_system_get_sections: - * - * return names of sections in applications menu. - * - * Returns: (element-type utf8) (transfer full): List of Names - */ -GList * -shell_app_system_get_sections (ShellAppSystem *system) -{ - GList *res = NULL; - GMenuTreeDirectory *root; - GMenuTreeIter *iter; - GMenuTreeItemType next_type; - - root = gmenu_tree_get_root_directory (system->priv->apps_tree); - - if (G_UNLIKELY (!root)) - g_error ("applications.menu not found."); - - iter = gmenu_tree_directory_iter (root); - - while ((next_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID) - { - if (next_type == GMENU_TREE_ITEM_DIRECTORY) - { - GMenuTreeDirectory *dir = gmenu_tree_iter_get_directory (iter); - char *name = g_strdup (gmenu_tree_directory_get_name (dir)); - - g_assert (name); - - res = g_list_append (res, name); - gmenu_tree_item_unref (dir); - } - } - - gmenu_tree_iter_unref (iter); - - return res; -} - -/** - * shell_app_info_get_section: - * - * return name of section, that contain this application. - * Returns: (transfer full): section name - */ -char * -shell_app_info_get_section (ShellAppInfo *info) -{ - char *name; - GMenuTreeDirectory *dir, *parent; - - if (info->type != SHELL_APP_INFO_TYPE_ENTRY) - return NULL; - - dir = gmenu_tree_entry_get_parent (info->entry); - if (!dir) - return NULL; - - parent = gmenu_tree_directory_get_parent (dir); - if (!parent) - return NULL; - - while (TRUE) - { - GMenuTreeDirectory *pparent = gmenu_tree_directory_get_parent (parent); - if (!pparent) - break; - gmenu_tree_item_unref (dir); - dir = parent; - parent = pparent; - } - - name = g_strdup (gmenu_tree_directory_get_name (dir)); - - gmenu_tree_item_unref (dir); - gmenu_tree_item_unref (parent); - return name; -} - -gboolean -shell_app_info_get_is_nodisplay (ShellAppInfo *info) -{ - switch (info->type) - { - case SHELL_APP_INFO_TYPE_ENTRY: - return g_desktop_app_info_get_nodisplay (gmenu_tree_entry_get_app_info (info->entry)); - case SHELL_APP_INFO_TYPE_DESKTOP_FILE: - case SHELL_APP_INFO_TYPE_WINDOW: - return FALSE; - } - g_assert_not_reached (); - return TRUE; -} - -/** - * shell_app_info_is_transient: - * - * A "transient" application is one which represents - * just an open window, i.e. we don't know how to launch it - * again. - */ -gboolean -shell_app_info_is_transient (ShellAppInfo *info) -{ - return info->type == SHELL_APP_INFO_TYPE_WINDOW; -} - -/** - * shell_app_info_create_icon_texture: - * - * Look up the icon for this application, and create a #ClutterTexture - * for it at the given size. - * - * Return value: (transfer none): A floating #ClutterActor - */ -ClutterActor * -shell_app_info_create_icon_texture (ShellAppInfo *info, float size) -{ - GIcon *icon; - ClutterActor *ret; - - ret = NULL; - - if (info->type == SHELL_APP_INFO_TYPE_WINDOW) - { - ret = st_texture_cache_bind_pixbuf_property (st_texture_cache_get_default (), - G_OBJECT (info->window), - "icon"); - } - else - { - icon = shell_app_info_get_icon (info); - if (icon != NULL) - { - ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size); - g_object_unref (icon); - } - } - - if (ret == NULL) - { - icon = g_themed_icon_new ("application-x-executable"); - ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size); - g_object_unref (icon); - } - - return ret; -} - -/** - * shell_app_info_get_source_window: - * @info: A #ShellAppInfo - * - * Returns: (transfer none): If @info is tracking a #MetaWindow, - * return that window. Otherwise, return %NULL. - */ -MetaWindow * -shell_app_info_get_source_window (ShellAppInfo *info) -{ - if (info->type == SHELL_APP_INFO_TYPE_WINDOW) - return info->window; - return NULL; + g_hash_table_insert (self->priv->entry_to_app, gmenu_tree_item_ref (entry), app); } static void -_gather_pid_callback (GDesktopAppInfo *gapp, - GPid pid, - gpointer data) +gather_apps_recurse (ShellAppSystem *self, + GMenuTreeDirectory *root) { - ShellApp *app; - ShellWindowTracker *tracker; + GMenuTreeIter *iter = gmenu_tree_directory_iter (root); + GMenuTreeItemType next_type; - g_return_if_fail (data != NULL); + while ((next_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID) + { + gpointer item = NULL; - app = SHELL_APP (data); - tracker = shell_window_tracker_get_default (); + switch (next_type) + { + case GMENU_TREE_ITEM_ENTRY: + { + item = gmenu_tree_iter_get_entry (iter); + load_app_entry (self, (GMenuTreeEntry*)item); + } + break; + case GMENU_TREE_ITEM_DIRECTORY: + { + item = gmenu_tree_iter_get_directory (iter); + gather_apps_recurse (self, (GMenuTreeDirectory*)item); + } + break; + default: + break; + } + if (item != NULL) + gmenu_tree_item_unref (item); + } - _shell_window_tracker_add_child_process_app (tracker, - pid, - app); + gmenu_tree_iter_unref (iter); +} + +static void +gather_settings_recurse (ShellAppSystem *self, + GMenuTreeDirectory *root) +{ + GMenuTreeIter *iter = gmenu_tree_directory_iter (root); + GMenuTreeItemType next_type; + + while ((next_type = gmenu_tree_iter_next (iter)) != GMENU_TREE_ITEM_INVALID) + { + gpointer item = NULL; + + switch (next_type) + { + case GMENU_TREE_ITEM_ENTRY: + { + ShellApp *app; + + item = gmenu_tree_iter_get_entry (iter); + if (g_hash_table_lookup (self->priv->setting_entry_to_app, item)) + return; + + app = _shell_app_new (item); + + g_hash_table_insert (self->priv->setting_entry_to_app, gmenu_tree_item_ref (item), app); + } + break; + case GMENU_TREE_ITEM_DIRECTORY: + { + item = gmenu_tree_iter_get_directory (iter); + gather_settings_recurse (self, (GMenuTreeDirectory*)item); + } + break; + default: + break; + } + if (item != NULL) + gmenu_tree_item_unref (item); + } + + gmenu_tree_iter_unref (iter); +} + +static void +on_apps_tree_changed_cb (GMenuTree *tree, + gpointer user_data) +{ + ShellAppSystem *self = SHELL_APP_SYSTEM (user_data); + GError *error = NULL; + GMenuTreeDirectory *root; + + g_assert (tree == self->priv->apps_tree); + + g_hash_table_remove_all (self->priv->entry_to_app); + g_slist_foreach (self->priv->known_vendor_prefixes, (GFunc)g_free, NULL); + g_slist_free (self->priv->known_vendor_prefixes); + self->priv->known_vendor_prefixes = NULL; + + if (!gmenu_tree_load_sync (self->priv->apps_tree, &error)) + { + g_warning ("Failed to load apps: %s", error->message); + return; + } + + root = gmenu_tree_get_root_directory (self->priv->apps_tree); + + if (root) + { + gather_apps_recurse (self, root); + gmenu_tree_item_unref (root); + } + + g_signal_emit (self, signals[INSTALLED_CHANGED], 0); +} + +static void +on_settings_tree_changed_cb (GMenuTree *tree, + gpointer user_data) +{ + ShellAppSystem *self = SHELL_APP_SYSTEM (user_data); + GError *error = NULL; + GMenuTreeDirectory *root; + + g_assert (tree == self->priv->settings_tree); + + g_hash_table_remove_all (self->priv->setting_entry_to_app); + if (!gmenu_tree_load_sync (self->priv->settings_tree, &error)) + { + g_warning ("Failed to load settings: %s", error->message); + return; + } + + root = gmenu_tree_get_root_directory (self->priv->settings_tree); + + if (root) + { + gather_settings_recurse (self, root); + gmenu_tree_item_unref (root); + } } /** - * shell_app_info_launch_full: - * @timestamp: Event timestamp, or 0 for current event timestamp - * @uris: List of uris to pass to application - * @workspace: Start on this workspace, or -1 for default - * @startup_id: (out): Returned startup notification ID, or %NULL if none - * @error: A #GError + * shell_app_system_get_tree: + * + * Return Value: (transfer none): The #GMenuTree for apps */ -gboolean -shell_app_info_launch_full (ShellAppInfo *info, - guint timestamp, - GList *uris, - int workspace, - char **startup_id, - GError **error) +GMenuTree * +shell_app_system_get_tree (ShellAppSystem *self) { - ShellApp *shell_app; - GDesktopAppInfo *gapp; - GdkAppLaunchContext *context; - gboolean ret; - ShellGlobal *global; - MetaScreen *screen; - - if (startup_id) - *startup_id = NULL; - - if (info->type == SHELL_APP_INFO_TYPE_WINDOW) - { - /* We can't pass URIs into a window; shouldn't hit this - * code path. If we do, fix the caller to disallow it. - */ - g_return_val_if_fail (uris == NULL, TRUE); - - meta_window_activate (info->window, timestamp); - return TRUE; - } - else if (info->type == SHELL_APP_INFO_TYPE_ENTRY) - { - /* Can't use g_desktop_app_info_new, see bug 614879 */ - const char *filename = gmenu_tree_entry_get_desktop_file_path (info->entry); - gapp = g_desktop_app_info_new_from_filename (filename); - } - else - { - char *filename = shell_app_info_get_desktop_file_path (info); - gapp = g_desktop_app_info_new_from_filename (filename); - g_free (filename); - } - - if (!gapp) - { - g_set_error (error, G_IO_ERROR, G_IO_ERROR_NOT_FOUND, "Not found"); - return FALSE; - } - - global = shell_global_get (); - screen = shell_global_get_screen (global); - - if (timestamp == 0) - timestamp = clutter_get_current_event_time (); - - if (workspace < 0) - workspace = meta_screen_get_active_workspace_index (screen); - - context = gdk_app_launch_context_new (); - gdk_app_launch_context_set_timestamp (context, timestamp); - gdk_app_launch_context_set_desktop (context, workspace); - - shell_app = shell_app_system_get_app (shell_app_system_get_default (), - shell_app_info_get_id (info)); - - /* In the case where we know an app, we handle reaping the child internally, - * in the window tracker. - */ - if (shell_app != NULL) - ret = g_desktop_app_info_launch_uris_as_manager (gapp, uris, - G_APP_LAUNCH_CONTEXT (context), - G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, - NULL, NULL, - _gather_pid_callback, shell_app, - error); - else - ret = g_desktop_app_info_launch_uris_as_manager (gapp, uris, - G_APP_LAUNCH_CONTEXT (context), - G_SPAWN_SEARCH_PATH, - NULL, NULL, - NULL, NULL, - error); - - g_object_unref (G_OBJECT (gapp)); - - return ret; + return self->priv->apps_tree; } -gboolean -shell_app_info_launch (ShellAppInfo *info, - GError **error) +/** + * shell_app_system_get_settings_tree: + * + * Return Value: (transfer none): The #GMenuTree for apps + */ +GMenuTree * +shell_app_system_get_settings_tree (ShellAppSystem *self) { - return shell_app_info_launch_full (info, 0, NULL, -1, NULL, error); + return self->priv->settings_tree; +} + +/** + * shell_app_system_lookup_setting: + * @self: + * @id: desktop file id + * + * Returns: (transfer none): Application in gnomecc.menu, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_setting (ShellAppSystem *self, + const char *id) +{ + GMenuTreeEntry *entry; + ShellApp *app; + + /* Actually defer to the main app set if there's overlap */ + app = shell_app_system_lookup_app (self, id); + if (app != NULL) + return app; + + entry = gmenu_tree_get_entry_by_id (self->priv->settings_tree, id); + if (entry == NULL) + return NULL; + + app = g_hash_table_lookup (self->priv->setting_entry_to_app, entry); + if (app != NULL) + return app; + + app = _shell_app_new (entry); + g_hash_table_insert (self->priv->setting_entry_to_app, gmenu_tree_item_ref (entry), app); + + return app; +} + +/** + * shell_app_system_get_default: + * + * Return Value: (transfer none): The global #ShellAppSystem singleton + */ +ShellAppSystem * +shell_app_system_get_default () +{ + static ShellAppSystem *instance = NULL; + + if (instance == NULL) + instance = g_object_new (SHELL_TYPE_APP_SYSTEM, NULL); + + return instance; +} + +/** + * shell_app_system_lookup_app: + * + * Find a #ShellApp corresponding to an id. + * + * Return value: (transfer none): The #ShellApp for id, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_app (ShellAppSystem *self, + const char *id) +{ + GMenuTreeEntry *entry; + + entry = gmenu_tree_get_entry_by_id (self->priv->apps_tree, id); + if (entry == NULL) + return NULL; + + return g_hash_table_lookup (self->priv->entry_to_app, entry); +} + +/** + * shell_app_system_lookup_app_by_tree_entry: + * @system: a #ShellAppSystem + * @entry: a #GMenuTreeEntry + * + * Find a #ShellApp corresponding to a #GMenuTreeEntry. + * + * Return value: (transfer none): The #ShellApp for @entry, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_app_by_tree_entry (ShellAppSystem *self, + GMenuTreeEntry *entry) +{ + return g_hash_table_lookup (self->priv->entry_to_app, entry); +} + +/** + * shell_app_system_lookup_app_for_path: + * @system: a #ShellAppSystem + * @desktop_path: (type utf8): UTF-8 encoded absolute file name + * + * Find or create a #ShellApp corresponding to a given absolute file + * name which must be in the standard paths (XDG_DATA_DIRS). For + * files outside the datadirs, this function returns %NULL. + * + * Return value: (transfer none): The #ShellApp for id, or %NULL if none + */ +ShellApp * +shell_app_system_lookup_app_for_path (ShellAppSystem *system, + const char *desktop_path) +{ + const char *basename; + const char *app_path; + ShellApp *app; + + basename = g_strrstr (desktop_path, "/"); + if (basename) + basename += 1; + else + basename = desktop_path; + + app = shell_app_system_lookup_app (system, basename); + if (!app) + return NULL; + + app_path = gmenu_tree_entry_get_desktop_file_path (shell_app_get_tree_entry (app)); + if (strcmp (desktop_path, app_path) != 0) + return NULL; + + return app; +} + +/** + * shell_app_system_lookup_heuristic_basename: + * @system: a #ShellAppSystem + * @id: Probable application identifier + * + * Find a valid application corresponding to a given + * heuristically determined application identifier + * string, or %NULL if none. + * + * Returns: (transfer none): A #ShellApp for @name + */ +ShellApp * +shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *name) +{ + ShellApp *result; + GSList *prefix; + + result = shell_app_system_lookup_app (system, name); + if (result != NULL) + return result; + + for (prefix = system->priv->known_vendor_prefixes; prefix; prefix = g_slist_next (prefix)) + { + char *tmpid = g_strconcat ((char*)prefix->data, name, NULL); + result = shell_app_system_lookup_app (system, tmpid); + g_free (tmpid); + if (result != NULL) + return result; + } + + return NULL; +} + +/** + * shell_app_system_get_all: + * @self: + * + * Returns: (transfer container) (element-type ShellApp): All installed applications + */ +GSList * +shell_app_system_get_all (ShellAppSystem *self) +{ + GSList *result = NULL; + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init (&iter, self->priv->entry_to_app); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + ShellApp *app = value; + result = g_slist_prepend (result, app); + } + return result; +} + +static gint +compare_apps_by_name (gconstpointer a, + gconstpointer b, + gpointer data) +{ + ShellApp *app_a = (ShellApp*)a; + ShellApp *app_b = (ShellApp*)b; + + return shell_app_compare_by_name (app_a, app_b); +} + +static GSList * +sort_and_concat_results (ShellAppSystem *system, + GSList *multiple_prefix_matches, + GSList *prefix_matches, + GSList *multiple_substring_matches, + GSList *substring_matches) +{ + multiple_prefix_matches = g_slist_sort_with_data (multiple_prefix_matches, + compare_apps_by_name, + system); + prefix_matches = g_slist_sort_with_data (prefix_matches, + compare_apps_by_name, + system); + multiple_substring_matches = g_slist_sort_with_data (multiple_substring_matches, + compare_apps_by_name, + system); + substring_matches = g_slist_sort_with_data (substring_matches, + compare_apps_by_name, + system); + return g_slist_concat (multiple_prefix_matches, g_slist_concat (prefix_matches, g_slist_concat (multiple_substring_matches, substring_matches))); +} + +static char * +normalize_and_casefold (const char *str) +{ + char *normalized, *result; + + if (str == NULL) + return NULL; + + normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL); + result = g_utf8_casefold (normalized, -1); + g_free (normalized); + return result; +} + +/** + * normalize_terms: + * @terms: (element-type utf8): Input search terms + * + * Returns: (element-type utf8) (transfer full): Unicode-normalized and lowercased terms + */ +static GSList * +normalize_terms (GSList *terms) +{ + GSList *normalized_terms = NULL; + GSList *iter; + for (iter = terms; iter; iter = iter->next) + { + const char *term = iter->data; + normalized_terms = g_slist_prepend (normalized_terms, normalize_and_casefold (term)); + } + return normalized_terms; +} + +static GSList * +search_tree (ShellAppSystem *self, + GSList *terms, + GHashTable *apps) +{ + GSList *multiple_prefix_results = NULL; + GSList *prefix_results = NULL; + GSList *multiple_subtring_results = NULL; + GSList *substring_results = NULL; + GSList *normalized_terms; + GHashTableIter iter; + gpointer key, value; + + normalized_terms = normalize_terms (terms); + + g_hash_table_iter_init (&iter, apps); + while (g_hash_table_iter_next (&iter, &key, &value)) + { + const char *id = key; + ShellApp *app = value; + (void)id; + _shell_app_do_match (app, normalized_terms, + &multiple_prefix_results, &prefix_results, + &multiple_subtring_results, &substring_results); + } + g_slist_foreach (normalized_terms, (GFunc)g_free, NULL); + g_slist_free (normalized_terms); + + return sort_and_concat_results (self, multiple_prefix_results, prefix_results, + multiple_subtring_results, substring_results); + +} + +/** + * shell_app_system_initial_search: + * @system: A #ShellAppSystem + * @terms: (element-type utf8): List of terms, logical AND + * + * Search through applications for the given search terms. + * + * Returns: (transfer container) (element-type ShellApp): List of applications + */ +GSList * +shell_app_system_initial_search (ShellAppSystem *self, + GSList *terms) +{ + return search_tree (self, terms, self->priv->entry_to_app); +} + +/** + * shell_app_system_subsearch: + * @system: A #ShellAppSystem + * @previous_results: (element-type ShellApp): List of previous results + * @terms: (element-type utf8): List of terms, logical AND + * + * Search through a previous result set; for more information, see + * js/ui/search.js. Note the value of @prefs must be + * the same as passed to shell_app_system_initial_search(). Note that returned + * strings are only valid until a return to the main loop. + * + * Returns: (transfer container) (element-type ShellApp): List of application identifiers + */ +GSList * +shell_app_system_subsearch (ShellAppSystem *system, + GSList *previous_results, + GSList *terms) +{ + GSList *iter; + GSList *multiple_prefix_results = NULL; + GSList *prefix_results = NULL; + GSList *multiple_substring_results = NULL; + GSList *substring_results = NULL; + GSList *normalized_terms = normalize_terms (terms); + + for (iter = previous_results; iter; iter = iter->next) + { + ShellApp *app = iter->data; + + _shell_app_do_match (app, normalized_terms, + &multiple_prefix_results, &prefix_results, + &multiple_substring_results, &substring_results); + } + g_slist_foreach (normalized_terms, (GFunc)g_free, NULL); + g_slist_free (normalized_terms); + + /* Note that a shorter term might have matched as a prefix, but + when extended only as a substring, so we have to redo the + sort rather than reusing the existing ordering */ + return sort_and_concat_results (system, multiple_prefix_results, prefix_results, multiple_substring_results, substring_results); +} + +/** + * shell_app_system_search_settings: + * @system: A #ShellAppSystem + * @terms: (element-type utf8): List of terms, logical AND + * + * Search through settings for the given search terms. + * + * Returns: (transfer container) (element-type ShellApp): List of setting applications + */ +GSList * +shell_app_system_search_settings (ShellAppSystem *self, + GSList *terms) +{ + return search_tree (self, terms, self->priv->setting_entry_to_app); } diff --git a/src/shell-app-system.h b/src/shell-app-system.h index b53c78d6d..a2a55f646 100644 --- a/src/shell-app-system.h +++ b/src/shell-app-system.h @@ -5,6 +5,8 @@ #include #include #include +#define GMENU_I_KNOW_THIS_IS_UNSTABLE +#include #include "shell-app.h" @@ -37,63 +39,33 @@ struct _ShellAppSystemClass GType shell_app_system_get_type (void) G_GNUC_CONST; ShellAppSystem *shell_app_system_get_default (void); -typedef struct _ShellAppInfo ShellAppInfo; +GMenuTree *shell_app_system_get_tree (ShellAppSystem *system); -#define SHELL_TYPE_APP_INFO (shell_app_info_get_type ()) -GType shell_app_info_get_type (void); - -ShellAppInfo *shell_app_info_ref (ShellAppInfo *info); -void shell_app_info_unref (ShellAppInfo *info); - -const char *shell_app_info_get_id (ShellAppInfo *info); -char *shell_app_info_get_name (ShellAppInfo *info); -char *shell_app_info_get_description (ShellAppInfo *info); -char *shell_app_info_get_executable (ShellAppInfo *info); -char *shell_app_info_get_desktop_file_path (ShellAppInfo *info); -GIcon *shell_app_info_get_icon (ShellAppInfo *info); -ClutterActor *shell_app_info_create_icon_texture (ShellAppInfo *info, - float size); -char *shell_app_info_get_section (ShellAppInfo *info); -gboolean shell_app_info_get_is_nodisplay (ShellAppInfo *info); -gboolean shell_app_info_is_transient (ShellAppInfo *info); -MetaWindow *shell_app_info_get_source_window (ShellAppInfo *info); - -gboolean shell_app_info_launch (ShellAppInfo *info, - GError **error); -gboolean shell_app_info_launch_full (ShellAppInfo *info, - guint timestamp, - GList *uris, - int workspace, - char **startup_id, - GError **error); +ShellApp *shell_app_system_lookup_app (ShellAppSystem *system, + const char *id); +ShellApp *shell_app_system_lookup_app_by_tree_entry (ShellAppSystem *system, + GMenuTreeEntry *entry); +ShellApp *shell_app_system_lookup_app_for_path (ShellAppSystem *system, + const char *desktop_path); +ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, + const char *id); -GList *shell_app_system_get_sections (ShellAppSystem *system); -GSList *shell_app_system_get_flattened_apps (ShellAppSystem *system); -GSList *shell_app_system_get_all_settings (ShellAppSystem *system); - -ShellApp *shell_app_system_get_app (ShellAppSystem *system, - const char *id); -ShellApp *shell_app_system_get_app_for_path (ShellAppSystem *system, - const char *desktop_path); -ShellApp *shell_app_system_get_app_for_window (ShellAppSystem *self, - MetaWindow *window); -ShellApp *shell_app_system_lookup_heuristic_basename (ShellAppSystem *system, - const char *id); - -ShellAppInfo *shell_app_system_create_from_window (ShellAppSystem *system, - MetaWindow *window); +GSList *shell_app_system_get_all (ShellAppSystem *system); GSList *shell_app_system_initial_search (ShellAppSystem *system, - gboolean prefs, GSList *terms); GSList *shell_app_system_subsearch (ShellAppSystem *system, - gboolean prefs, GSList *previous_results, GSList *terms); -/* internal API */ -void _shell_app_system_register_app (ShellAppSystem *self, - ShellApp *app); +GMenuTree *shell_app_system_get_settings_tree (ShellAppSystem *system); + +GSList *shell_app_system_search_settings (ShellAppSystem *system, + GSList *terms); + +ShellApp *shell_app_system_lookup_setting (ShellAppSystem *system, + const char *id); + #endif /* __SHELL_APP_SYSTEM_H__ */ diff --git a/src/shell-app-usage.c b/src/shell-app-usage.c index f9e91b02c..698dc8f19 100644 --- a/src/shell-app-usage.c +++ b/src/shell-app-usage.c @@ -105,7 +105,7 @@ G_DEFINE_TYPE (ShellAppUsage, shell_app_usage, G_TYPE_OBJECT); struct UsageData { /* Whether the application we're tracking is "transient", see - * shell_app_info_is_transient. + * shell_app_is_window_backed. */ gboolean transient; @@ -315,7 +315,7 @@ on_app_state_changed (ShellWindowTracker *tracker, UsageData *usage; gboolean running; - if (shell_app_is_transient (app)) + if (shell_app_is_window_backed (app)) return; usage = get_usage_for_app (self, app); @@ -509,7 +509,7 @@ shell_app_usage_get_most_used (ShellAppUsage *self, const char *appid = iter->data; ShellApp *app; - app = shell_app_system_get_app (appsys, appid); + app = shell_app_system_lookup_app (appsys, appid); if (!app) continue; @@ -670,7 +670,7 @@ idle_save_application_usage (gpointer data) { ShellApp *app; - app = shell_app_system_get_app (shell_app_system_get_default(), id); + app = shell_app_system_lookup_app (shell_app_system_get_default(), id); if (!app) continue; diff --git a/src/shell-app.c b/src/shell-app.c index 301f01e97..0f74716ec 100644 --- a/src/shell-app.c +++ b/src/shell-app.c @@ -14,6 +14,14 @@ #include "shell-window-tracker-private.h" #include "st.h" +typedef enum { + MATCH_NONE, + MATCH_SUBSTRING, /* Not prefix, substring */ + MATCH_MULTIPLE_SUBSTRING, /* Matches multiple criteria with substrings */ + MATCH_PREFIX, /* Strict prefix */ + MATCH_MULTIPLE_PREFIX, /* Matches multiple criteria, at least one prefix */ +} ShellAppSearchMatch; + /* This is mainly a memory usage optimization - the user is going to * be running far fewer of the applications at one time than they have * installed. But it also just helps keep the code more logically @@ -38,7 +46,7 @@ typedef struct { * SECTION:shell-app * @short_description: Object representing an application * - * This object wraps a #ShellAppInfo, providing methods and signals + * This object wraps a #GMenuTreeEntry, providing methods and signals * primarily useful for running applications. */ struct _ShellApp @@ -49,9 +57,16 @@ struct _ShellApp ShellAppState state; - ShellAppInfo *info; + GMenuTreeEntry *entry; /* If NULL, this app is backend by exactly one MetaWindow */ ShellAppRunningState *running_state; + + char *window_id_string; + + char *casefolded_name; + char *name_collation_key; + char *casefolded_description; + char *casefolded_exec; }; G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT); @@ -93,7 +108,18 @@ shell_app_get_property (GObject *gobject, const char * shell_app_get_id (ShellApp *app) { - return shell_app_info_get_id (app->info); + if (app->entry) + return gmenu_tree_entry_get_desktop_file_id (app->entry); + return app->window_id_string; +} + +static MetaWindow * +window_backed_app_get_window (ShellApp *app) +{ + g_assert (app->entry == NULL); + g_assert (app->running_state); + g_assert (app->running_state->windows); + return app->running_state->windows->data; } /** @@ -108,8 +134,33 @@ ClutterActor * shell_app_create_icon_texture (ShellApp *app, float size) { - return shell_app_info_create_icon_texture (app->info, size); + GIcon *icon; + ClutterActor *ret; + + ret = NULL; + + if (app->entry == NULL) + { + MetaWindow *window = window_backed_app_get_window (app); + return st_texture_cache_bind_pixbuf_property (st_texture_cache_get_default (), + G_OBJECT (window), + "icon"); + } + + icon = g_app_info_get_icon (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry))); + if (icon != NULL) + ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size); + + if (ret == NULL) + { + icon = g_themed_icon_new ("application-x-executable"); + ret = st_texture_cache_load_gicon (st_texture_cache_get_default (), NULL, icon, (int)size); + g_object_unref (icon); + } + + return ret; } + typedef struct { ShellApp *app; int size; @@ -143,13 +194,12 @@ shell_app_create_faded_icon_cpu (StTextureCache *cache, info = NULL; - icon = shell_app_info_get_icon (app->info); + icon = g_app_info_get_icon (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry))); if (icon != NULL) { info = gtk_icon_theme_lookup_by_gicon (gtk_icon_theme_get_default (), icon, (int) (size + 0.5), GTK_ICON_LOOKUP_FORCE_SIZE); - g_object_unref (icon); } if (info == NULL) @@ -224,22 +274,23 @@ shell_app_create_faded_icon_cpu (StTextureCache *cache, ClutterActor * shell_app_get_faded_icon (ShellApp *app, float size) { - MetaWindow *window; CoglHandle texture; ClutterActor *result; char *cache_key; CreateFadedIconData data; - /* Punt for WINDOW types for now...easier to reuse the property tracking bits, - * and this helps us visually distinguish app-tracked from not. + /* Don't fade for window backed apps for now...easier to reuse the + * property tracking bits, and this helps us visually distinguish + * app-tracked from not. */ - window = shell_app_info_get_source_window (app->info); - if (window) + if (!app->entry) { + MetaWindow *window = window_backed_app_get_window (app); return st_texture_cache_bind_pixbuf_property (st_texture_cache_get_default (), G_OBJECT (window), "icon"); } + cache_key = g_strdup_printf ("faded-icon:%s,size=%f", shell_app_get_id (app), size); data.app = app; @@ -266,22 +317,43 @@ shell_app_get_faded_icon (ShellApp *app, float size) return result; } -char * +const char * shell_app_get_name (ShellApp *app) { - return shell_app_info_get_name (app->info); + if (app->entry) + return g_app_info_get_name (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry))); + else + { + MetaWindow *window = window_backed_app_get_window (app); + const char *name; + + name = meta_window_get_wm_class (window); + if (!name) + name = _("Unknown"); + return name; + } } -char * +const char * shell_app_get_description (ShellApp *app) { - return shell_app_info_get_description (app->info); + if (app->entry) + return g_app_info_get_description (G_APP_INFO (gmenu_tree_entry_get_app_info (app->entry))); + else + return NULL; } +/** + * shell_app_is_window_backed: + * + * A window backed application is one which represents just an open + * window, i.e. there's no .desktop file assocation, so we don't know + * how to launch it again. + */ gboolean -shell_app_is_transient (ShellApp *app) +shell_app_is_window_backed (ShellApp *app) { - return shell_app_info_is_transient (app->info); + return app->entry == NULL; } typedef struct { @@ -453,12 +525,12 @@ shell_app_activate (ShellApp *app, case SHELL_APP_STATE_STOPPED: { GError *error = NULL; - if (!shell_app_info_launch_full (app->info, - 0, - NULL, - workspace, - NULL, - &error)) + if (!shell_app_launch (app, + 0, + NULL, + workspace, + NULL, + &error)) { char *msg; msg = g_strdup_printf (_("Failed to launch '%s'"), shell_app_get_name (app)); @@ -489,6 +561,8 @@ void shell_app_open_new_window (ShellApp *app, int workspace) { + g_return_if_fail (app->entry != NULL); + /* Here we just always launch the application again, even if we know * it was already running. For most applications this * should have the effect of creating a new window, whether that's @@ -497,12 +571,12 @@ shell_app_open_new_window (ShellApp *app, * as say Pidgin. Ideally, we have the application express to us * that it supports an explicit new-window action. */ - shell_app_info_launch_full (app->info, - 0, - NULL, - workspace, - NULL, - NULL); + shell_app_launch (app, + 0, + NULL, + workspace, + NULL, + NULL); } /** @@ -517,17 +591,6 @@ shell_app_get_state (ShellApp *app) return app->state; } -/** - * _shell_app_get_info: - * - * Returns: (transfer none): Associated app info - */ -ShellAppInfo * -_shell_app_get_info (ShellApp *app) -{ - return app->info; -} - typedef struct { ShellApp *app; MetaWorkspace *active_workspace; @@ -697,21 +760,26 @@ _shell_app_new_for_window (MetaWindow *window) ShellApp *app; 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); + + /* For windows, its id is simply its pointer address as a string. + * There are various other alternatives, but the address is unique + * and unchanging, which is pretty much the best we can do. + */ + app->window_id_string = g_strdup_printf ("window:%p", window); + _shell_app_add_window (app, window); return app; } ShellApp * -_shell_app_new (ShellAppInfo *info) +_shell_app_new (GMenuTreeEntry *info) { ShellApp *app; 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); + app->entry = gmenu_tree_item_ref (info); + app->name_collation_key = g_utf8_collate_key (shell_app_get_name (app), -1); return app; } @@ -922,6 +990,112 @@ shell_app_request_quit (ShellApp *app) return TRUE; } +static void +_gather_pid_callback (GDesktopAppInfo *gapp, + GPid pid, + gpointer data) +{ + ShellApp *app; + ShellWindowTracker *tracker; + + g_return_if_fail (data != NULL); + + app = SHELL_APP (data); + tracker = shell_window_tracker_get_default (); + + _shell_window_tracker_add_child_process_app (tracker, + pid, + app); +} + +/** + * shell_app_launch: + * @timestamp: Event timestamp, or 0 for current event timestamp + * @uris: List of uris to pass to application + * @workspace: Start on this workspace, or -1 for default + * @startup_id: (out): Returned startup notification ID, or %NULL if none + * @error: A #GError + */ +gboolean +shell_app_launch (ShellApp *app, + guint timestamp, + GList *uris, + int workspace, + char **startup_id, + GError **error) +{ + GDesktopAppInfo *gapp; + GdkAppLaunchContext *context; + gboolean ret; + ShellGlobal *global; + MetaScreen *screen; + + if (startup_id) + *startup_id = NULL; + + if (app->entry == NULL) + { + MetaWindow *window = window_backed_app_get_window (app); + /* We can't pass URIs into a window; shouldn't hit this + * code path. If we do, fix the caller to disallow it. + */ + g_return_val_if_fail (uris == NULL, TRUE); + + meta_window_activate (window, timestamp); + return TRUE; + } + + global = shell_global_get (); + screen = shell_global_get_screen (global); + + if (timestamp == 0) + timestamp = clutter_get_current_event_time (); + + if (workspace < 0) + workspace = meta_screen_get_active_workspace_index (screen); + + context = gdk_app_launch_context_new (); + gdk_app_launch_context_set_timestamp (context, timestamp); + gdk_app_launch_context_set_desktop (context, workspace); + + gapp = gmenu_tree_entry_get_app_info (app->entry); + ret = g_desktop_app_info_launch_uris_as_manager (gapp, uris, + G_APP_LAUNCH_CONTEXT (context), + G_SPAWN_SEARCH_PATH | G_SPAWN_DO_NOT_REAP_CHILD, + NULL, NULL, + _gather_pid_callback, app, + error); + g_object_unref (context); + + return ret; +} + +/** + * shell_app_get_app_info: + * @app: a #ShellApp + * + * Returns: (transfer none): The #GDesktopAppInfo for this app, or %NULL if backed by a window + */ +GDesktopAppInfo * +shell_app_get_app_info (ShellApp *app) +{ + if (app->entry) + return gmenu_tree_entry_get_app_info (app->entry); + return NULL; +} + +/** + * shell_app_get_tree_entry: + * @app: a #ShellApp + * + * Returns: (transfer none): The #GMenuTreeEntry for this app, or %NULL if backed by a window + */ +GMenuTreeEntry * +shell_app_get_tree_entry (ShellApp *app) +{ + return app->entry; +} + static void create_running_state (ShellApp *app) { @@ -951,6 +1125,171 @@ unref_running_state (ShellAppRunningState *state) g_slice_free (ShellAppRunningState, state); } +static char * +normalize_and_casefold (const char *str) +{ + char *normalized, *result; + + if (str == NULL) + return NULL; + + normalized = g_utf8_normalize (str, -1, G_NORMALIZE_ALL); + result = g_utf8_casefold (normalized, -1); + g_free (normalized); + return result; +} + +static char * +trim_exec_line (const char *str) +{ + const char *start, *end, *pos; + + end = strchr (str, ' '); + if (end == NULL) + end = str + strlen (str); + + start = str; + while ((pos = strchr (start, '/')) && pos < end) + start = ++pos; + + return g_strndup (start, end - start); +} + +static void +shell_app_init_search_data (ShellApp *app) +{ + const char *name; + const char *exec; + const char *comment; + char *normalized_exec; + GDesktopAppInfo *appinfo; + + appinfo = gmenu_tree_entry_get_app_info (app->entry); + name = g_app_info_get_name (G_APP_INFO (appinfo)); + app->casefolded_name = normalize_and_casefold (name); + + comment = g_app_info_get_description (G_APP_INFO (appinfo)); + app->casefolded_description = normalize_and_casefold (comment); + + exec = g_app_info_get_executable (G_APP_INFO (appinfo)); + normalized_exec = normalize_and_casefold (exec); + app->casefolded_exec = trim_exec_line (normalized_exec); + g_free (normalized_exec); +} + +/** + * shell_app_compare_by_name: + * @app: + * @other: + * + * Order two applications by name. + * + * Returns: -1, 0, or 1; suitable for use as a comparison function for e.g. g_slist_sort() + */ +int +shell_app_compare_by_name (ShellApp *app, ShellApp *other) +{ + return strcmp (app->name_collation_key, other->name_collation_key); +} + +static ShellAppSearchMatch +_shell_app_match_search_terms (ShellApp *app, + GSList *terms) +{ + GSList *iter; + ShellAppSearchMatch match; + + if (G_UNLIKELY (!app->casefolded_name)) + shell_app_init_search_data (app); + + match = MATCH_NONE; + for (iter = terms; iter; iter = iter->next) + { + ShellAppSearchMatch current_match; + const char *term = iter->data; + const char *p; + + current_match = MATCH_NONE; + + p = strstr (app->casefolded_name, term); + if (p == app->casefolded_name) + current_match = MATCH_PREFIX; + else if (p != NULL) + current_match = MATCH_SUBSTRING; + + p = strstr (app->casefolded_exec, term); + if (p != NULL) + { + if (p == app->casefolded_exec) + current_match = (current_match == MATCH_NONE) ? MATCH_PREFIX + : MATCH_MULTIPLE_PREFIX; + else if (current_match < MATCH_PREFIX) + current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING + : MATCH_MULTIPLE_SUBSTRING; + } + + if (app->casefolded_description && current_match < MATCH_PREFIX) + { + /* Only do substring matches, as prefix matches are not meaningful + * enough for descriptions + */ + p = strstr (app->casefolded_description, term); + if (p != NULL) + current_match = (current_match == MATCH_NONE) ? MATCH_SUBSTRING + : MATCH_MULTIPLE_SUBSTRING; + } + + if (current_match == MATCH_NONE) + return current_match; + + if (current_match > match) + match = current_match; + } + return match; +} + +void +_shell_app_do_match (ShellApp *app, + GSList *terms, + GSList **multiple_prefix_results, + GSList **prefix_results, + GSList **multiple_substring_results, + GSList **substring_results) +{ + ShellAppSearchMatch match; + GAppInfo *appinfo; + + g_assert (app != NULL); + + /* Skip window-backed apps */ + appinfo = (GAppInfo*)shell_app_get_app_info (app); + if (appinfo == NULL) + return; + /* Skip not-visible apps */ + if (!g_app_info_should_show (appinfo)) + return; + + match = _shell_app_match_search_terms (app, terms); + switch (match) + { + case MATCH_NONE: + break; + case MATCH_MULTIPLE_PREFIX: + *multiple_prefix_results = g_slist_prepend (*multiple_prefix_results, app); + break; + case MATCH_PREFIX: + *prefix_results = g_slist_prepend (*prefix_results, app); + break; + case MATCH_MULTIPLE_SUBSTRING: + *multiple_substring_results = g_slist_prepend (*multiple_substring_results, app); + break; + case MATCH_SUBSTRING: + *substring_results = g_slist_prepend (*substring_results, app); + break; + } +} + + static void shell_app_init (ShellApp *self) { @@ -962,10 +1301,10 @@ shell_app_dispose (GObject *object) { ShellApp *app = SHELL_APP (object); - if (app->info) + if (app->entry) { - shell_app_info_unref (app->info); - app->info = NULL; + gmenu_tree_item_unref (app->entry); + app->entry = NULL; } if (app->running_state) @@ -977,6 +1316,20 @@ shell_app_dispose (GObject *object) G_OBJECT_CLASS(shell_app_parent_class)->dispose (object); } +static void +shell_app_finalize (GObject *object) +{ + ShellApp *app = SHELL_APP (object); + + g_free (app->window_id_string); + + g_free (app->casefolded_name); + g_free (app->name_collation_key); + g_free (app->casefolded_description); + + G_OBJECT_CLASS(shell_app_parent_class)->finalize (object); +} + static void shell_app_class_init(ShellAppClass *klass) { @@ -984,6 +1337,7 @@ shell_app_class_init(ShellAppClass *klass) gobject_class->get_property = shell_app_get_property; gobject_class->dispose = shell_app_dispose; + gobject_class->finalize = shell_app_finalize; shell_app_signals[WINDOWS_CHANGED] = g_signal_new ("windows-changed", SHELL_TYPE_APP, diff --git a/src/shell-app.h b/src/shell-app.h index 729cfecc8..d1aed0231 100644 --- a/src/shell-app.h +++ b/src/shell-app.h @@ -5,6 +5,8 @@ #include #include #include +#define GMENU_I_KNOW_THIS_IS_UNSTABLE +#include G_BEGIN_DECLS @@ -34,12 +36,14 @@ typedef enum { GType shell_app_get_type (void) G_GNUC_CONST; const char *shell_app_get_id (ShellApp *app); +GMenuTreeEntry *shell_app_get_tree_entry (ShellApp *app); +GDesktopAppInfo *shell_app_get_app_info (ShellApp *app); ClutterActor *shell_app_create_icon_texture (ShellApp *app, float size); ClutterActor *shell_app_get_faded_icon (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); +const char *shell_app_get_name (ShellApp *app); +const char *shell_app_get_description (ShellApp *app); +gboolean shell_app_is_window_backed (ShellApp *app); void shell_app_activate_window (ShellApp *app, MetaWindow *window, guint32 timestamp); @@ -61,6 +65,15 @@ GSList *shell_app_get_pids (ShellApp *app); gboolean shell_app_is_on_workspace (ShellApp *app, MetaWorkspace *workspace); +gboolean shell_app_launch (ShellApp *app, + guint timestamp, + GList *uris, + int workspace, + char **startup_id, + GError **error); + +int shell_app_compare_by_name (ShellApp *app, ShellApp *other); + int shell_app_compare (ShellApp *app, ShellApp *other); G_END_DECLS diff --git a/src/shell-window-tracker.c b/src/shell-window-tracker.c index 31d3e009d..b864f10e7 100644 --- a/src/shell-window-tracker.c +++ b/src/shell-window-tracker.c @@ -18,7 +18,6 @@ #include #include "shell-window-tracker-private.h" -#include "shell-app-system.h" #include "shell-app-private.h" #include "shell-global.h" #include "shell-marshal.h" @@ -244,6 +243,8 @@ get_app_from_window_wmclass (MetaWindow *window) g_free (wmclass); app = shell_app_system_lookup_heuristic_basename (appsys, with_desktop); + if (app != NULL) + g_object_ref (app); g_free (with_desktop); return app; @@ -340,19 +341,16 @@ get_app_from_window_pid (ShellWindowTracker *tracker, */ static ShellApp * get_app_for_window (ShellWindowTracker *monitor, - MetaWindow *window) + MetaWindow *window) { - ShellApp *result; + ShellApp *result = NULL; const char *startup_id; - if (meta_window_is_remote (window)) - return shell_app_system_get_app_for_window (shell_app_system_get_default (), window); - - result = NULL; /* First, we check whether we already know about this window, * if so, just return that. */ - if (meta_window_get_window_type (window) == META_WINDOW_NORMAL) + if (meta_window_get_window_type (window) == META_WINDOW_NORMAL + || meta_window_is_remote (window)) { result = g_hash_table_lookup (monitor->window_to_app, window); if (result != NULL) @@ -362,6 +360,9 @@ get_app_for_window (ShellWindowTracker *monitor, } } + if (meta_window_is_remote (window)) + return _shell_app_new_for_window (window); + /* Check if the app's WM_CLASS specifies an app; this is * canonical if it does. */ @@ -389,7 +390,10 @@ get_app_for_window (ShellWindowTracker *monitor, result = shell_startup_sequence_get_app (sequence); if (result) - break; + { + result = g_object_ref (result); + break; + } } } @@ -401,7 +405,7 @@ get_app_for_window (ShellWindowTracker *monitor, /* Our last resort - we create a fake app from the window */ if (result == NULL) - result = shell_app_system_get_app_for_window (shell_app_system_get_default (), window); + result = _shell_app_new_for_window (window); return result; } @@ -855,7 +859,7 @@ shell_startup_sequence_get_id (ShellStartupSequence *sequence) * shell_startup_sequence_get_app: * @sequence: A #ShellStartupSequence * - * Returns: (transfer full): The application being launched, or %NULL if unknown. + * Returns: (transfer none): The application being launched, or %NULL if unknown. */ ShellApp * shell_startup_sequence_get_app (ShellStartupSequence *sequence) @@ -869,7 +873,7 @@ shell_startup_sequence_get_app (ShellStartupSequence *sequence) return NULL; appsys = shell_app_system_get_default (); - app = shell_app_system_get_app_for_path (appsys, appid); + app = shell_app_system_lookup_app_for_path (appsys, appid); return app; }