[gnome-shell] Split appDisplay and docDisplay into "model" and "view" parts



commit a3d35af444445b92b3fe2475ebef4e282b8c34d1
Author: Dan Winship <danw gnome org>
Date:   Tue Jun 16 12:20:12 2009 -0400

    Split appDisplay and docDisplay into "model" and "view" parts
    
    This lets us share the recent-app-tracking, recent-file-tracking, and
    icon-drawing code between the overlay and the sidebar, without the
    sidebar having to poke into AppDisplayItem and DocDisplayItem's guts.

 configure.ac        |    1 +
 js/Makefile.am      |    2 +-
 js/misc/Makefile.am |    5 +
 js/misc/appInfo.js  |  133 +++++++++++++++++++++++++++++++
 js/misc/docInfo.js  |  112 ++++++++++++++++++++++++++
 js/ui/appDisplay.js |   72 +++---------------
 js/ui/docDisplay.js |  114 ++++-----------------------
 js/ui/widget.js     |  216 +++++++++++++++++++++++++++++---------------------
 8 files changed, 404 insertions(+), 251 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index b01e6aa..1e448b6 100644
--- a/configure.ac
+++ b/configure.ac
@@ -101,6 +101,7 @@ AC_OUTPUT([
   Makefile
   data/Makefile
   js/Makefile
+  js/misc/Makefile
   js/ui/Makefile
   src/Makefile
 ])
diff --git a/js/Makefile.am b/js/Makefile.am
index aac7812..43d6f51 100644
--- a/js/Makefile.am
+++ b/js/Makefile.am
@@ -1 +1 @@
-SUBDIRS = ui
+SUBDIRS = misc ui
diff --git a/js/misc/Makefile.am b/js/misc/Makefile.am
new file mode 100644
index 0000000..434dc3a
--- /dev/null
+++ b/js/misc/Makefile.am
@@ -0,0 +1,5 @@
+jsmiscdir = $(pkgdatadir)/js/misc
+
+dist_jsmisc_DATA =		\
+	appInfo.js		\
+	docInfo.js
diff --git a/js/misc/appInfo.js b/js/misc/appInfo.js
new file mode 100644
index 0000000..c7bf9c7
--- /dev/null
+++ b/js/misc/appInfo.js
@@ -0,0 +1,133 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Shell = imports.gi.Shell;
+
+const Main = imports.ui.main;
+
+// TODO - move this into GConf once we're not a plugin anymore
+// but have taken over metacity
+// This list is taken from GNOME Online popular applications
+// http://online.gnome.org/applications
+// but with nautilus removed (since it should already be running)
+// and evince, totem, and gnome-file-roller removed (since they're
+// usually started by opening documents, not by opening the app
+// directly)
+const DEFAULT_APPLICATIONS = [
+    'mozilla-firefox.desktop',
+    'gnome-terminal.desktop',
+    'evolution.desktop',
+    'gedit.desktop',
+    'mozilla-thunderbird.desktop',
+    'rhythmbox.desktop',
+    'epiphany.desktop',
+    'xchat.desktop',
+    'openoffice.org-1.9-writer.desktop',
+    'emacs.desktop',
+    'gnome-system-monitor.desktop',
+    'openoffice.org-1.9-calc.desktop',
+    'eclipse.desktop',
+    'openoffice.org-1.9-impress.desktop',
+    'vncviewer.desktop'
+];
+
+function AppInfo(appId) {
+    this._init(appId);
+}
+
+AppInfo.prototype = {
+    _init : function(appId) {
+        this.appId = appId;
+        this._gAppInfo = Gio.DesktopAppInfo.new(appId);
+        if (!this._gAppInfo)
+            throw new Error('Unknown appId ' + appId);
+
+        this.name = this._gAppInfo.get_name();
+        this.description = this._gAppInfo.get_description();
+
+        this._gicon = this._gAppInfo.get_icon();
+    },
+
+    getIcon : function(size) {
+        if (this._gicon)
+            return Shell.TextureCache.get_default().load_gicon(this._gicon, size);
+        else
+            return new Clutter.Texture({ width: size, height: size });
+    },
+
+    getIconPath : function(size) {
+        if (this._gicon) {
+            let iconTheme = Gtk.IconTheme.get_default();
+            let previewIconInfo = iconTheme.lookup_by_gicon(this._gicon, size, 0);
+            if (previewIconInfo)
+                return previewIconInfo.get_filename();
+        }
+        return null;
+    },
+
+    launch : function() {
+        this._gAppInfo.launch([], Main.createAppLaunchContext());
+    }
+};
+
+var _infos = {};
+
+// getAppInfo:
+// @appId: an appId
+//
+// Gets an #AppInfo for @appId. This is preferable to calling
+// new AppInfo() directly, because it caches #AppInfos.
+//
+// Return value: the new or cached #AppInfo, or %null if @appId
+// doesn't point to a valid .desktop file
+function getAppInfo(appId) {
+    let info = _infos[appId];
+    if (info === undefined) {
+        try {
+            info = _infos[appId] = new AppInfo(appId);
+        } catch (e) {
+            info = _infos[appId] = null;
+        }
+    }
+    return info;
+}
+
+// getMostUsedApps:
+// @count: maximum number of apps to retrieve
+//
+// Gets a list of #AppInfos for the @count most-frequently-used
+// applications
+//
+// Return value: the list of #AppInfo
+function getMostUsedApps(count) {
+    let appMonitor = new Shell.AppMonitor();
+
+    // Ask for more apps than we need, since the list of recently used
+    // apps might contain an app we don't have a desktop file for
+    let apps = appMonitor.get_most_used_apps (0, Math.round(count * 1.5));
+    let matches = [], alreadyAdded = {};
+
+    for (let i = 0; i < apps.length && matches.length <= count; i++) {
+        let appId = apps[i] + ".desktop";
+        let appInfo = getAppInfo(appId);
+        if (appInfo) {
+            matches.push(appInfo);
+            alreadyAdded[appId] = true;
+        }
+    }
+
+    // Fill the list with default applications it's not full yet
+    for (let i = 0; i < DEFAULT_APPLICATIONS.length && matches.length <= count; i++) {
+        let appId = DEFAULT_APPLICATIONS[i];
+        if (alreadyAdded[appId])
+            continue;
+
+        let appInfo = getAppInfo(appId);
+        if (appInfo)
+            matches.push(appInfo);
+    }
+
+    return matches;
+}
diff --git a/js/misc/docInfo.js b/js/misc/docInfo.js
new file mode 100644
index 0000000..dfae4a7
--- /dev/null
+++ b/js/misc/docInfo.js
@@ -0,0 +1,112 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Shell = imports.gi.Shell;
+
+const Main = imports.ui.main;
+
+const THUMBNAIL_ICON_MARGIN = 2;
+
+function DocInfo(recentInfo) {
+    this._init(recentInfo);
+}
+
+DocInfo.prototype = {
+    _init : function(recentInfo) {
+        this._recentInfo = recentInfo;
+        this.name = recentInfo.get_display_name();
+        this.uri = recentInfo.get_uri();
+        this.mimeType = recentInfo.get_mime_type();
+
+        this._iconPixbuf = Shell.get_thumbnail(this.uri, this.mimeType);
+    },
+
+    getIcon : function(size) {
+        let icon = new Clutter.Texture();
+
+        if (this._iconPixbuf) {
+            // We calculate the width and height of the texture so as
+            // to preserve the aspect ratio of the thumbnail. Because
+            // the images generated based on thumbnails don't have an
+            // internal padding like system icons do, we create a
+            // slightly smaller texture and then create a group around
+            // it for padding purposes
+
+            let scalingFactor = (size - THUMBNAIL_ICON_MARGIN * 2) / Math.max(this._iconPixbuf.get_width(), this._iconPixbuf.get_height());
+            icon.set_width(Math.ceil(this._iconPixbuf.get_width() * scalingFactor));
+            icon.set_height(Math.ceil(this._iconPixbuf.get_height() * scalingFactor));
+            Shell.clutter_texture_set_from_pixbuf(icon, this._iconPixbuf);
+
+            let group = new Clutter.Group({ width: size,
+                                            height: size });
+            group.add_actor(icon);
+            icon.set_position(THUMBNAIL_ICON_MARGIN, THUMBNAIL_ICON_MARGIN);
+            return group;
+        } else {
+            Shell.clutter_texture_set_from_pixbuf(icon, this._recentInfo.get_icon(size));
+            return icon;
+        }
+    },
+
+    launch : function() {
+        // While using Gio.app_info_launch_default_for_uri() would be
+        // shorter in terms of lines of code, we are not doing so
+        // because that would duplicate the work of retrieving the
+        // mime type.
+
+        let appInfo = Gio.app_info_get_default_for_type(this.mimeType, true);
+
+        if (appInfo != null) {
+            appInfo.launch_uris([this.uri], Main.createAppLaunchContext());
+        } else {
+            log("Failed to get default application info for mime type " + mimeType +
+                ". Will try to use the last application that registered the document.");
+            let appName = this._recentInfo.last_application();
+            let [success, appExec, count, time] = this._recentInfo.get_application_info(appName);
+            if (success) {
+                log("Will open a document with the following command: " + appExec);
+                // TODO: Change this once better support for creating
+                // GAppInfo is added to GtkRecentInfo, as right now
+                // this relies on the fact that the file uri is
+                // already a part of appExec, so we don't supply any
+                // files to appInfo.launch().
+
+                // The 'command line' passed to
+                // create_from_command_line is allowed to contain
+                // '%<something>' macros that are expanded to file
+                // name / icon name, etc, so we need to escape % as %%
+                appExec = appExec.replace(/%/g, "%%");
+
+                let appInfo = Gio.app_info_create_from_commandline(appExec, null, 0, null);
+
+                // The point of passing an app launch context to
+                // launch() is mostly to get startup notification and
+                // associated benefits like the app appearing on the
+                // right desktop; but it doesn't really work for now
+                // because with the way we create the appInfo we
+                // aren't reading the application's desktop file, and
+                // thus don't find the StartupNotify=true in it. So,
+                // despite passing the app launch context, no startup
+                // notification occurs.
+                appInfo.launch([], Main.createAppLaunchContext());
+            } else {
+                log("Failed to get application info for " + this.uri);
+            }
+        }
+    },
+
+    exists : function() {
+        return this._recentInfo.exists();
+    },
+
+    lastVisited : function() {
+        // We actually used get_modified() instead of get_visited()
+        // here, as GtkRecentInfo doesn't updated get_visited()
+        // correctly. See
+        // http://bugzilla.gnome.org/show_bug.cgi?id=567094
+
+        return this._recentInfo.get_modified();
+    }
+};
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index d0ba6bb..ab53772 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -9,8 +9,8 @@ const Shell = imports.gi.Shell;
 const Lang = imports.lang;
 const Signals = imports.signals;
 
+const AppInfo = imports.misc.appInfo;
 const GenericDisplay = imports.ui.genericDisplay;
-const Main = imports.ui.main;
 
 const ENTERED_MENU_COLOR = new Clutter.Color();
 ENTERED_MENU_COLOR.from_pixel(0x00ff0022);
@@ -48,7 +48,7 @@ const MAX_ITEMS = 30;
 
 /* This class represents a single display item containing information about an application.
  *
- * appInfo - GAppInfo object containing information about the application
+ * appInfo - AppInfo object containing information about the application
  * availableWidth - total width available for the item
  */
 function AppDisplayItem(appInfo, availableWidth) {
@@ -62,36 +62,15 @@ AppDisplayItem.prototype = {
         GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth); 
         this._appInfo = appInfo;
 
-        let name = appInfo.get_name();
-
-        let description = appInfo.get_description();
-
-        let iconTheme = Gtk.IconTheme.get_default();
-
-        let gicon = appInfo.get_icon();
-        let texCache = Shell.TextureCache.get_default();
-        let icon;
-        if (gicon == null)
-            icon = new Clutter.Texture({ width: GenericDisplay.ITEM_DISPLAY_ICON_SIZE,
-                                         height: GenericDisplay.ITEM_DISPLAY_ICON_SIZE
-                                       });
-        else
-            icon = texCache.load_gicon(gicon, GenericDisplay.ITEM_DISPLAY_ICON_SIZE);
-        this._setItemInfo(name, description, icon);
-    },
-
-    //// Public methods ////
-
-    // Returns the application info associated with this display item.
-    getAppInfo : function () {
-        return this._appInfo;
+        this._setItemInfo(appInfo.name, appInfo.description,
+                          appInfo.getIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE));
     },
 
     //// Public method overrides ////
 
     // Opens an application represented by this display item.
     launch : function() {
-        this._appInfo.launch([], Main.createAppLaunchContext());
+        this._appInfo.launch();
     },
 
     //// Protected method overrides ////
@@ -101,15 +80,7 @@ AppDisplayItem.prototype = {
         if (!this._showPreview || this._previewIcon)
             return; 
 
-        let previewIconPath = null;
-
-        if (this._gicon != null) {
-            let iconTheme = Gtk.IconTheme.get_default();
-            let previewIconInfo = iconTheme.lookup_by_gicon(this._gicon, GenericDisplay.PREVIEW_ICON_SIZE, Gtk.IconLookupFlags.NO_SVG);
-            if (previewIconInfo)
-                previewIconPath = previewIconInfo.get_filename();
-        }
-
+        let previewIconPath = this._appInfo.getIconPath(GenericDisplay.PREVIEW_ICON_SIZE);
         if (previewIconPath) {
             try {
                 this._previewIcon = new Clutter.Texture({ width: GenericDisplay.PREVIEW_ICON_SIZE, height: GenericDisplay.PREVIEW_ICON_SIZE});               
@@ -346,7 +317,7 @@ AppDisplay.prototype = {
     },
 
     _addApp: function(appId) {
-        let appInfo = Gio.DesktopAppInfo.new(appId);
+        let appInfo = AppInfo.getAppInfo(appId);
         if (appInfo != null) {
             this._allItems[appId] = appInfo;
             // [] is returned if we could not get the categories or the list of categories was empty
@@ -393,31 +364,8 @@ AppDisplay.prototype = {
 
     // Sets the list of the displayed items based on the most used apps.
     _setDefaultList : function() {
-        // Ask or more app than we need, since the list of recently used apps
-        // might contain an app we don't have a desktop file for
-        var apps = this._appMonitor.get_most_used_apps (0, Math.round(MAX_ITEMS * 1.5));
-        this._matchedItems = [];
-        for (let i = 0; i < apps.length; i++) {
-            if (this._matchedItems.length > MAX_ITEMS)
-                break;
-            let appId = apps[i] + ".desktop";
-            let appInfo = this._allItems[appId];
-            if (appInfo) {
-                this._matchedItems.push(appId);
-            }
-        }
-        
-        // Fill the list with default applications it's not full yet
-        for (let i = 0; i < DEFAULT_APPLICATIONS.length; i++) {
-              if (this._matchedItems.length > MAX_ITEMS)
-                  break;
-              let appId = DEFAULT_APPLICATIONS[i];
-              let appInfo = this._allItems[appId];
-              // Don't add if not available or already present in the list
-              if (appInfo && (this._matchedItems.indexOf(appId) == -1)) {
-                  this._matchedItems.push(appId);
-            }
-        }
+        let matchedInfos = AppInfo.getMostUsedApps(MAX_ITEMS);
+        this._matchedItems = matchedInfos.map(function(info) { return info.appId; });
     },
 
     // Compares items associated with the item ids based on the alphabetical order
@@ -486,7 +434,7 @@ AppDisplay.prototype = {
         return false;
     },
 
-    // Creates an AppDisplayItem based on itemInfo, which is expected be a GAppInfo object.
+    // Creates an AppDisplayItem based on itemInfo, which is expected be an AppInfo object.
     _createDisplayItem: function(itemInfo) {
         return new AppDisplayItem(itemInfo, this._columnWidth);
     }
diff --git a/js/ui/docDisplay.js b/js/ui/docDisplay.js
index 070fe62..2ea2820 100644
--- a/js/ui/docDisplay.js
+++ b/js/ui/docDisplay.js
@@ -7,14 +7,13 @@ const Lang = imports.lang;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 
+const DocInfo = imports.misc.docInfo;
 const GenericDisplay = imports.ui.genericDisplay;
 const Main = imports.ui.main;
 
-const ITEM_DISPLAY_ICON_MARGIN = 2;
-
 /* This class represents a single display item containing information about a document.
  *
- * docInfo - GtkRecentInfo object containing information about the document
+ * docInfo - DocInfo object containing information about the document
  * availableWidth - total width available for the item
  */
 function DocDisplayItem(docInfo, availableWidth) {
@@ -28,107 +27,34 @@ DocDisplayItem.prototype = {
         GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);     
         this._docInfo = docInfo;
     
-        let name = docInfo.get_display_name();
-
-        // we can possibly display tags in the space for description in the future
-        let description = ""; 
-
-        let icon = new Clutter.Texture();
-        this._iconPixbuf = Shell.get_thumbnail(docInfo.get_uri(), docInfo.get_mime_type());
-        if (this._iconPixbuf) {
-            // We calculate the width and height of the texture so as to preserve the aspect ratio of the thumbnail.
-            // Because the images generated based on thumbnails don't have an internal padding like system icons do,
-            // we create a slightly smaller texture and then use extra margin when positioning it. 
-            let scalingFactor = (GenericDisplay.ITEM_DISPLAY_ICON_SIZE - ITEM_DISPLAY_ICON_MARGIN * 2) / Math.max(this._iconPixbuf.get_width(), this._iconPixbuf.get_height());
-            icon.set_width(Math.ceil(this._iconPixbuf.get_width() * scalingFactor));
-            icon.set_height(Math.ceil(this._iconPixbuf.get_height() * scalingFactor));
-            Shell.clutter_texture_set_from_pixbuf(icon, this._iconPixbuf);
-            icon.x = GenericDisplay.ITEM_DISPLAY_PADDING + ITEM_DISPLAY_ICON_MARGIN;
-            icon.y = GenericDisplay.ITEM_DISPLAY_PADDING + ITEM_DISPLAY_ICON_MARGIN;       
-        } else {
-            Shell.clutter_texture_set_from_pixbuf(icon, docInfo.get_icon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE));
-            icon.x = GenericDisplay.ITEM_DISPLAY_PADDING;
-            icon.y = GenericDisplay.ITEM_DISPLAY_PADDING;
-        } 
-
-        this._setItemInfo(name, description, icon);
+        this._setItemInfo(docInfo.name, "",
+                          docInfo.getIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE));
     },
 
     //// Public methods ////
 
-    // Returns the document info associated with this display item.
-    getDocInfo : function() {
-        return this._docInfo;
-    },
- 
     //// Public method overrides ////
 
     // Opens a document represented by this display item.
     launch : function() {
-        // While using Gio.app_info_launch_default_for_uri() would be shorter
-        // in terms of lines of code, we are not doing so because that would 
-        // duplicate the work of retrieving the mime type.       
-        let mimeType = this._docInfo.get_mime_type();
-        let appInfo = Gio.app_info_get_default_for_type(mimeType, true);
-
-        if (appInfo != null) {
-            appInfo.launch_uris([this._docInfo.get_uri()], Main.createAppLaunchContext());
-        } else {
-            log("Failed to get default application info for mime type " + mimeType + 
-                ". Will try to use the last application that registered the document."); 
-            let appName = this._docInfo.last_application();
-            let [success, appExec, count, time] = this._docInfo.get_application_info(appName);
-            if (success) {
-                log("Will open a document with the following command: " + appExec);
-                // TODO: Change this once better support for creating GAppInfo is added to 
-                // GtkRecentInfo, as right now this relies on the fact that the file uri is
-                // already a part of appExec, so we don't supply any files to appInfo.launch().
-
-                // The 'command line' passed to create_from_command_line is allowed to contain
-                // '%<something>' macros that are expanded to file name / icon name, etc,
-                // so we need to escape % as %%
-                appExec = appExec.replace(/%/g, "%%");
-
-                let appInfo = Gio.app_info_create_from_commandline(appExec, null, 0, null);
-
-                // The point of passing an app launch context to launch() is mostly to get
-                // startup notification and associated benefits like the app appearing
-                // on the right desktop; but it doesn't really work for now because with
-                // the way we create the appInfo we aren't reading the application's desktop 
-                // file, and thus don't find the StartupNotify=true in it. So, despite passing 
-                // the app launch context, no startup notification occurs.
-                appInfo.launch([], Main.createAppLaunchContext());
-            } else {
-                log("Failed to get application info for " + this._docInfo.get_uri());
-            }
-        }
+        this._docInfo.launch();
     },
 
     //// Protected method overrides ////
 
     // Ensures the preview icon is created.
     _ensurePreviewIconCreated : function() {
-        if (this._previewIcon)
-            return; 
-
-        this._previewIcon = new Clutter.Texture();
-        if (this._iconPixbuf) {
-            let scalingFactor = (GenericDisplay.PREVIEW_ICON_SIZE / Math.max(this._iconPixbuf.get_width(), this._iconPixbuf.get_height()));
-            this._previewIcon.set_width(Math.ceil(this._iconPixbuf.get_width() * scalingFactor));
-            this._previewIcon.set_height(Math.ceil(this._iconPixbuf.get_height() * scalingFactor));
-            Shell.clutter_texture_set_from_pixbuf(this._previewIcon, this._iconPixbuf);           
-        } else {
-            Shell.clutter_texture_set_from_pixbuf(this._previewIcon, this._docInfo.get_icon(GenericDisplay.PREVIEW_ICON_SIZE));
-        }
+        if (!this._previewIcon)
+            this._previewIcon = this._docInfo.getIcon(GenericDisplay.PREVIEW_ICON_SIZE);
     },
 
     // Creates and returns a large preview icon, but only if this._docInfo is an image file
     // and we were able to generate a pixbuf from it successfully.
     _createLargePreviewIcon : function(availableWidth, availableHeight) {
-        if (this._docInfo.get_mime_type() == null || this._docInfo.get_mime_type().indexOf("image/") != 0)
+        if (this._docInfo.mimeType == null || this._docInfo.mimeType.indexOf("image/") != 0)
             return null;
 
-        return Shell.TextureCache.get_default().load_uri_sync(this._docInfo.get_uri(), availableWidth, availableHeight);
+        return Shell.TextureCache.get_default().load_uri_sync(this._docInfo.uri, availableWidth, availableHeight);
     }
 };
 
@@ -170,10 +96,11 @@ DocDisplay.prototype = {
         this._allItems = {};
         let docs = this._recentManager.get_items();
         for (let i = 0; i < docs.length; i++) {
-            let docInfo = docs[i];
-            let docId = docInfo.get_uri();
+            let recentInfo = docs[i];
+            let docInfo = new DocInfo.DocInfo(recentInfo);
+
             // we use GtkRecentInfo URI as an item Id
-            this._allItems[docId] = docInfo;
+            this._allItems[docInfo.uri] = docInfo;
         }
         this._docsStale = false;
     },
@@ -217,15 +144,8 @@ DocDisplay.prototype = {
    _compareItems : function(itemIdA, itemIdB) {
         let docA = this._allItems[itemIdA];
         let docB = this._allItems[itemIdB];
-        // We actually used get_modified() instead of get_visited() here, as GtkRecentInfo
-        // doesn't updated get_visited() correctly.
-        // See http://bugzilla.gnome.org/show_bug.cgi?id=567094
-        if (docA.get_modified() > docB.get_modified())
-            return -1;
-        else if (docA.get_modified() < docB.get_modified())
-            return 1;
-        else
-            return 0;
+
+        return docB.lastVisited() - docA.lastVisited();
     },
 
     // Checks if the item info can be a match for the search string by checking
@@ -238,7 +158,7 @@ DocDisplay.prototype = {
         if (search == null || search == '')
             return true;
 
-        let name = itemInfo.get_display_name().toLowerCase();
+        let name = itemInfo.name.toLowerCase();
         if (name.indexOf(search) >= 0)
             return true;
         // TODO: we can also check doc URIs, so that
@@ -246,7 +166,7 @@ DocDisplay.prototype = {
         return false;
     },
 
-    // Creates a DocDisplayItem based on itemInfo, which is expected be a GtkRecentInfo object. 
+    // Creates a DocDisplayItem based on itemInfo, which is expected to be a DocInfo object.
     _createDisplayItem: function(itemInfo) {
         return new DocDisplayItem(itemInfo, this._columnWidth);
     } 
diff --git a/js/ui/widget.js b/js/ui/widget.js
index 2e38bf0..a49db31 100644
--- a/js/ui/widget.js
+++ b/js/ui/widget.js
@@ -6,11 +6,13 @@ const Gio = imports.gi.Gio;
 const Gtk = imports.gi.Gtk;
 const Mainloop = imports.mainloop;
 const Lang = imports.lang;
+const Pango = imports.gi.Pango;
 const Shell = imports.gi.Shell;
 const Signals = imports.signals;
 
-const AppDisplay = imports.ui.appDisplay;
+const AppInfo = imports.misc.appInfo;
 const DocDisplay = imports.ui.docDisplay;
+const DocInfo = imports.misc.docInfo;
 
 const COLLAPSED_WIDTH = 24;
 const EXPANDED_WIDTH = 200;
@@ -156,17 +158,118 @@ ClockWidget.prototype = {
 };
 
 
+const ITEM_ICON_SIZE = 48;
+const ITEM_PADDING = 1;
+const ITEM_SPACING = 4;
+
 const ITEM_BG_COLOR = new Clutter.Color();
 ITEM_BG_COLOR.from_pixel(0x00000000);
 const ITEM_NAME_COLOR = new Clutter.Color();
 ITEM_NAME_COLOR.from_pixel(0x000000ff);
-const ITEM_DESCRIPTION_COLOR = new Clutter.Color();
-ITEM_DESCRIPTION_COLOR.from_pixel(0x404040ff);
 
-function hackUpDisplayItemColors(item) {
-    item._bg.background_color = ITEM_BG_COLOR;
-    item._name.color = ITEM_NAME_COLOR;
-    item._description.color = ITEM_DESCRIPTION_COLOR;
+function LauncherWidget() {
+    this._init();
+}
+
+LauncherWidget.prototype = {
+    __proto__ : Widget.prototype,
+
+    addItem : function(info) {
+        let item = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
+                                 width: EXPANDED_WIDTH,
+                                 height: ITEM_ICON_SIZE,
+                                 padding: ITEM_PADDING,
+                                 spacing: ITEM_SPACING,
+                                 reactive: true });
+        item._info = info;
+        item.append(info.getIcon(ITEM_ICON_SIZE), Big.BoxPackFlags.NONE);
+        item.append(new Clutter.Text({ color: ITEM_NAME_COLOR,
+                                       font_name: "Sans 14px",
+                                       ellipsize: Pango.EllipsizeMode.END,
+                                       text: info.name }),
+                    Big.BoxPackFlags.NONE);
+
+        this.actor.append(item, Big.BoxPackFlags.NONE);
+        item.connect('button-press-event', Lang.bind(this, this._buttonPress));
+        item.connect('button-release-event', Lang.bind(this, this._buttonRelease));
+        item.connect('leave-event', Lang.bind(this, this._leave));
+        item.connect('enter-event', Lang.bind(this, this._enter));
+
+        if (!this.collapsedActor)
+            return;
+
+        item = new Big.Box({ width: COLLAPSED_WIDTH,
+                             height: COLLAPSED_WIDTH,
+                             padding: ITEM_PADDING,
+                             reactive: true });
+        item._info = info;
+        item.append(info.getIcon(COLLAPSED_WIDTH - 2 * ITEM_PADDING),
+                    Big.BoxPackFlags.NONE);
+
+        this.collapsedActor.append(item, Big.BoxPackFlags.NONE);
+        item.connect('button-press-event', Lang.bind(this, this._buttonPress));
+        item.connect('button-release-event', Lang.bind(this, this._buttonRelease));
+        item.connect('leave-event', Lang.bind(this, this._leave));
+        item.connect('enter-event', Lang.bind(this, this._enter));
+    },
+
+    clear : function() {
+        let children, i;
+
+        children = this.actor.get_children();
+        for (i = 0; i < children.length; i++)
+            children[i].destroy();
+
+        if (this.collapsedActor) {
+            children = this.collapsedActor.get_children();
+            for (i = 0; i < children.length; i++)
+                children[i].destroy();
+        }
+    },
+
+    _buttonPress : function(item) {
+        Clutter.grab_pointer(item);
+        item._buttonDown = true;
+        item._inItem = true;
+        this._updateItemState(item);
+        return true;
+    },
+
+    _leave : function(item, evt) {
+        if (evt.get_source() == item && item._buttonDown) {
+            item._inItem = false;
+            this._updateItemState(item);
+        }
+        return false;
+    },
+
+    _enter : function(item, evt) {
+        if (evt.get_source() == item && item._buttonDown) {
+            item._inItem = true;
+            this._updateItemState(item);
+        }
+        return false;
+    },
+
+    _buttonRelease : function(item) {
+        Clutter.ungrab_pointer(item);
+        item._buttonDown = false;
+        this._updateItemState(item);
+
+        if (item._inItem) {
+            item._info.launch();
+            this.activated();
+        }
+        return true;
+    },
+
+    _updateItemState : function(item) {
+        if (item._buttonDown && item._inItem) {
+            item.padding_top = item.padding_left = 2 * ITEM_PADDING;
+            item.padding_bottom = item.padding_right = 0;
+        } else
+            item.padding = ITEM_PADDING;
+    }
 };
 
 function AppsWidget() {
@@ -174,44 +277,16 @@ function AppsWidget() {
 }
 
 AppsWidget.prototype = {
-    __proto__ : Widget.prototype,
+    __proto__ : LauncherWidget.prototype,
 
     _init : function() {
         this.title = "Applications";
         this.actor = new Big.Box({ spacing: 2 });
         this.collapsedActor = new Big.Box({ spacing: 2});
 
-        let added = 0;
-        for (let i = 0; i < AppDisplay.DEFAULT_APPLICATIONS.length && added < 5; i++) {
-            let id = AppDisplay.DEFAULT_APPLICATIONS[i];
-            let appInfo = Gio.DesktopAppInfo.new(id);
-            if (!appInfo)
-                continue;
-
-            let box = new Big.Box({ padding: 2,
-                                    corner_radius: 2 });
-            let appDisplayItem = new AppDisplay.AppDisplayItem(
-                appInfo, EXPANDED_WIDTH);
-            hackUpDisplayItemColors(appDisplayItem);
-            box.append(appDisplayItem.actor, Big.BoxPackFlags.NONE);
-            this.actor.append(box, Big.BoxPackFlags.NONE);
-            appDisplayItem.connect('select', Lang.bind(this, this._itemActivated));
-
-            // Cheaty cheat cheat
-            let icon = new Clutter.Clone({ source: appDisplayItem._icon,
-                                           width: COLLAPSED_WIDTH,
-                                           height: COLLAPSED_WIDTH,
-                                           reactive: true });
-            this.collapsedActor.append(icon, Big.BoxPackFlags.NONE);
-            icon.connect('button-release-event', Lang.bind(this, function() { this._itemActivated(appDisplayItem); }));
-
-            added++;
-        }
-    },
-
-    _itemActivated: function(item) {
-        item.launch();
-        this.activated();
+        let apps = AppInfo.getMostUsedApps(5);
+        for (let i = 0; i < apps.length; i++)
+            this.addItem(apps[i]);
     }
 };
 
@@ -220,7 +295,7 @@ function DocsWidget() {
 }
 
 DocsWidget.prototype = {
-    __proto__ : Widget.prototype,
+    __proto__ : LauncherWidget.prototype,
 
     _init : function() {
         this.title = "Recent Docs";
@@ -232,62 +307,21 @@ DocsWidget.prototype = {
     },
 
     _recentChanged: function() {
-        let i, docId;
+        let i;
 
-        this._allItems = {};
+        this.clear();
+
+        let items = [];
         let docs = this._recentManager.get_items();
         for (i = 0; i < docs.length; i++) {
-            let docInfo = docs[i];
-            let docId = docInfo.get_uri();
-            // we use GtkRecentInfo URI as an item Id
-            this._allItems[docId] = docInfo;
-        }
-
-        this._matchedItems = [];
-        let docIdsToRemove = [];
-        for (docId in this._allItems) {
-            // this._allItems[docId].exists() checks if the resource still exists
-            if (this._allItems[docId].exists())
-                this._matchedItems.push(docId);
-            else
-                docIdsToRemove.push(docId);
-        }
-
-        for (docId in docIdsToRemove) {
-            delete this._allItems[docId];
-        }
+            let docInfo = new DocInfo.DocInfo (docs[i]);
 
-        this._matchedItems.sort(Lang.bind(this, function (a,b) { return this._compareItems(a,b); }));
-
-        let children = this.actor.get_children();
-        for (let c = 0; c < children.length; c++)
-            this.actor.remove_actor(children[c]);
-
-        for (i = 0; i < Math.min(this._matchedItems.length, 5); i++) {
-            let box = new Big.Box({ padding: 2,
-                                    corner_radius: 2 });
-            let docDisplayItem = new DocDisplay.DocDisplayItem(
-                this._allItems[this._matchedItems[i]], EXPANDED_WIDTH);
-            hackUpDisplayItemColors(docDisplayItem);
-            box.append(docDisplayItem.actor, Big.BoxPackFlags.NONE);
-            this.actor.append(box, Big.BoxPackFlags.NONE);
-            docDisplayItem.connect('select', Lang.bind(this, this._itemActivated));
+            if (docInfo.exists())
+                items.push(docInfo);
         }
-    },
-
-   _compareItems : function(itemIdA, itemIdB) {
-        let docA = this._allItems[itemIdA];
-        let docB = this._allItems[itemIdB];
-        if (docA.get_modified() > docB.get_modified())
-            return -1;
-        else if (docA.get_modified() < docB.get_modified())
-            return 1;
-        else
-            return 0;
-    },
 
-    _itemActivated: function(item) {
-        item.launch();
-        this.activated();
+        items.sort(function (a,b) { return b.lastVisited() - a.lastVisited(); });
+        for (i = 0; i < Math.min(items.length, 5); i++)
+            this.addItem(items[i]);
     }
 };



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]