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.
This commit is contained in:
Dan Winship 2009-06-16 12:20:12 -04:00
parent 4314c6e57f
commit a3d35af444
8 changed files with 405 additions and 252 deletions

View File

@ -101,6 +101,7 @@ AC_OUTPUT([
Makefile Makefile
data/Makefile data/Makefile
js/Makefile js/Makefile
js/misc/Makefile
js/ui/Makefile js/ui/Makefile
src/Makefile src/Makefile
]) ])

View File

@ -1 +1 @@
SUBDIRS = ui SUBDIRS = misc ui

5
js/misc/Makefile.am Normal file
View File

@ -0,0 +1,5 @@
jsmiscdir = $(pkgdatadir)/js/misc
dist_jsmisc_DATA = \
appInfo.js \
docInfo.js

133
js/misc/appInfo.js Normal file
View File

@ -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;
}

112
js/misc/docInfo.js Normal file
View File

@ -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();
}
};

View File

@ -9,8 +9,8 @@ const Shell = imports.gi.Shell;
const Lang = imports.lang; const Lang = imports.lang;
const Signals = imports.signals; const Signals = imports.signals;
const AppInfo = imports.misc.appInfo;
const GenericDisplay = imports.ui.genericDisplay; const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main;
const ENTERED_MENU_COLOR = new Clutter.Color(); const ENTERED_MENU_COLOR = new Clutter.Color();
ENTERED_MENU_COLOR.from_pixel(0x00ff0022); 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. /* 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 * availableWidth - total width available for the item
*/ */
function AppDisplayItem(appInfo, availableWidth) { function AppDisplayItem(appInfo, availableWidth) {
@ -62,36 +62,15 @@ AppDisplayItem.prototype = {
GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth); GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
this._appInfo = appInfo; this._appInfo = appInfo;
let name = appInfo.get_name(); this._setItemInfo(appInfo.name, appInfo.description,
appInfo.getIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE));
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;
}, },
//// Public method overrides //// //// Public method overrides ////
// Opens an application represented by this display item. // Opens an application represented by this display item.
launch : function() { launch : function() {
this._appInfo.launch([], Main.createAppLaunchContext()); this._appInfo.launch();
}, },
//// Protected method overrides //// //// Protected method overrides ////
@ -101,15 +80,7 @@ AppDisplayItem.prototype = {
if (!this._showPreview || this._previewIcon) if (!this._showPreview || this._previewIcon)
return; return;
let previewIconPath = null; let previewIconPath = this._appInfo.getIconPath(GenericDisplay.PREVIEW_ICON_SIZE);
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();
}
if (previewIconPath) { if (previewIconPath) {
try { try {
this._previewIcon = new Clutter.Texture({ width: GenericDisplay.PREVIEW_ICON_SIZE, height: GenericDisplay.PREVIEW_ICON_SIZE}); this._previewIcon = new Clutter.Texture({ width: GenericDisplay.PREVIEW_ICON_SIZE, height: GenericDisplay.PREVIEW_ICON_SIZE});
@ -346,7 +317,7 @@ AppDisplay.prototype = {
}, },
_addApp: function(appId) { _addApp: function(appId) {
let appInfo = Gio.DesktopAppInfo.new(appId); let appInfo = AppInfo.getAppInfo(appId);
if (appInfo != null) { if (appInfo != null) {
this._allItems[appId] = appInfo; this._allItems[appId] = appInfo;
// [] is returned if we could not get the categories or the list of categories was empty // [] 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. // Sets the list of the displayed items based on the most used apps.
_setDefaultList : function() { _setDefaultList : function() {
// Ask or more app than we need, since the list of recently used apps let matchedInfos = AppInfo.getMostUsedApps(MAX_ITEMS);
// might contain an app we don't have a desktop file for this._matchedItems = matchedInfos.map(function(info) { return info.appId; });
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);
}
}
}, },
// Compares items associated with the item ids based on the alphabetical order // Compares items associated with the item ids based on the alphabetical order
@ -486,7 +434,7 @@ AppDisplay.prototype = {
return false; 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) { _createDisplayItem: function(itemInfo) {
return new AppDisplayItem(itemInfo, this._columnWidth); return new AppDisplayItem(itemInfo, this._columnWidth);
} }

View File

@ -7,14 +7,13 @@ const Lang = imports.lang;
const Shell = imports.gi.Shell; const Shell = imports.gi.Shell;
const Signals = imports.signals; const Signals = imports.signals;
const DocInfo = imports.misc.docInfo;
const GenericDisplay = imports.ui.genericDisplay; const GenericDisplay = imports.ui.genericDisplay;
const Main = imports.ui.main; const Main = imports.ui.main;
const ITEM_DISPLAY_ICON_MARGIN = 2;
/* This class represents a single display item containing information about a document. /* 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 * availableWidth - total width available for the item
*/ */
function DocDisplayItem(docInfo, availableWidth) { function DocDisplayItem(docInfo, availableWidth) {
@ -28,107 +27,34 @@ DocDisplayItem.prototype = {
GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth); GenericDisplay.GenericDisplayItem.prototype._init.call(this, availableWidth);
this._docInfo = docInfo; this._docInfo = docInfo;
let name = docInfo.get_display_name(); this._setItemInfo(docInfo.name, "",
docInfo.getIcon(GenericDisplay.ITEM_DISPLAY_ICON_SIZE));
// 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);
}, },
//// Public methods //// //// Public methods ////
// Returns the document info associated with this display item.
getDocInfo : function() {
return this._docInfo;
},
//// Public method overrides //// //// Public method overrides ////
// Opens a document represented by this display item. // Opens a document represented by this display item.
launch : function() { launch : function() {
// While using Gio.app_info_launch_default_for_uri() would be shorter this._docInfo.launch();
// 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());
}
}
}, },
//// Protected method overrides //// //// Protected method overrides ////
// Ensures the preview icon is created. // Ensures the preview icon is created.
_ensurePreviewIconCreated : function() { _ensurePreviewIconCreated : function() {
if (this._previewIcon) if (!this._previewIcon)
return; this._previewIcon = this._docInfo.getIcon(GenericDisplay.PREVIEW_ICON_SIZE);
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));
}
}, },
// Creates and returns a large preview icon, but only if this._docInfo is an image file // 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. // and we were able to generate a pixbuf from it successfully.
_createLargePreviewIcon : function(availableWidth, availableHeight) { _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 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 = {}; this._allItems = {};
let docs = this._recentManager.get_items(); let docs = this._recentManager.get_items();
for (let i = 0; i < docs.length; i++) { for (let i = 0; i < docs.length; i++) {
let docInfo = docs[i]; let recentInfo = docs[i];
let docId = docInfo.get_uri(); let docInfo = new DocInfo.DocInfo(recentInfo);
// we use GtkRecentInfo URI as an item Id // we use GtkRecentInfo URI as an item Id
this._allItems[docId] = docInfo; this._allItems[docInfo.uri] = docInfo;
} }
this._docsStale = false; this._docsStale = false;
}, },
@ -217,15 +144,8 @@ DocDisplay.prototype = {
_compareItems : function(itemIdA, itemIdB) { _compareItems : function(itemIdA, itemIdB) {
let docA = this._allItems[itemIdA]; let docA = this._allItems[itemIdA];
let docB = this._allItems[itemIdB]; let docB = this._allItems[itemIdB];
// We actually used get_modified() instead of get_visited() here, as GtkRecentInfo
// doesn't updated get_visited() correctly. return docB.lastVisited() - docA.lastVisited();
// 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;
}, },
// Checks if the item info can be a match for the search string by checking // 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 == '') if (search == null || search == '')
return true; return true;
let name = itemInfo.get_display_name().toLowerCase(); let name = itemInfo.name.toLowerCase();
if (name.indexOf(search) >= 0) if (name.indexOf(search) >= 0)
return true; return true;
// TODO: we can also check doc URIs, so that // TODO: we can also check doc URIs, so that
@ -246,7 +166,7 @@ DocDisplay.prototype = {
return false; 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) { _createDisplayItem: function(itemInfo) {
return new DocDisplayItem(itemInfo, this._columnWidth); return new DocDisplayItem(itemInfo, this._columnWidth);
} }

View File

@ -6,11 +6,13 @@ const Gio = imports.gi.Gio;
const Gtk = imports.gi.Gtk; const Gtk = imports.gi.Gtk;
const Mainloop = imports.mainloop; const Mainloop = imports.mainloop;
const Lang = imports.lang; const Lang = imports.lang;
const Pango = imports.gi.Pango;
const Shell = imports.gi.Shell; const Shell = imports.gi.Shell;
const Signals = imports.signals; const Signals = imports.signals;
const AppDisplay = imports.ui.appDisplay; const AppInfo = imports.misc.appInfo;
const DocDisplay = imports.ui.docDisplay; const DocDisplay = imports.ui.docDisplay;
const DocInfo = imports.misc.docInfo;
const COLLAPSED_WIDTH = 24; const COLLAPSED_WIDTH = 24;
const EXPANDED_WIDTH = 200; 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(); const ITEM_BG_COLOR = new Clutter.Color();
ITEM_BG_COLOR.from_pixel(0x00000000); ITEM_BG_COLOR.from_pixel(0x00000000);
const ITEM_NAME_COLOR = new Clutter.Color(); const ITEM_NAME_COLOR = new Clutter.Color();
ITEM_NAME_COLOR.from_pixel(0x000000ff); ITEM_NAME_COLOR.from_pixel(0x000000ff);
const ITEM_DESCRIPTION_COLOR = new Clutter.Color();
ITEM_DESCRIPTION_COLOR.from_pixel(0x404040ff);
function hackUpDisplayItemColors(item) { function LauncherWidget() {
item._bg.background_color = ITEM_BG_COLOR; this._init();
item._name.color = ITEM_NAME_COLOR; }
item._description.color = ITEM_DESCRIPTION_COLOR;
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() { function AppsWidget() {
@ -174,44 +277,16 @@ function AppsWidget() {
} }
AppsWidget.prototype = { AppsWidget.prototype = {
__proto__ : Widget.prototype, __proto__ : LauncherWidget.prototype,
_init : function() { _init : function() {
this.title = "Applications"; this.title = "Applications";
this.actor = new Big.Box({ spacing: 2 }); this.actor = new Big.Box({ spacing: 2 });
this.collapsedActor = new Big.Box({ spacing: 2}); this.collapsedActor = new Big.Box({ spacing: 2});
let added = 0; let apps = AppInfo.getMostUsedApps(5);
for (let i = 0; i < AppDisplay.DEFAULT_APPLICATIONS.length && added < 5; i++) { for (let i = 0; i < apps.length; i++)
let id = AppDisplay.DEFAULT_APPLICATIONS[i]; this.addItem(apps[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();
} }
}; };
@ -220,7 +295,7 @@ function DocsWidget() {
} }
DocsWidget.prototype = { DocsWidget.prototype = {
__proto__ : Widget.prototype, __proto__ : LauncherWidget.prototype,
_init : function() { _init : function() {
this.title = "Recent Docs"; this.title = "Recent Docs";
@ -232,62 +307,21 @@ DocsWidget.prototype = {
}, },
_recentChanged: function() { _recentChanged: function() {
let i, docId; let i;
this._allItems = {}; this.clear();
let items = [];
let docs = this._recentManager.get_items(); let docs = this._recentManager.get_items();
for (i = 0; i < docs.length; i++) { for (i = 0; i < docs.length; i++) {
let docInfo = docs[i]; let docInfo = new DocInfo.DocInfo (docs[i]);
let docId = docInfo.get_uri();
// we use GtkRecentInfo URI as an item Id if (docInfo.exists())
this._allItems[docId] = docInfo; items.push(docInfo);
} }
this._matchedItems = []; items.sort(function (a,b) { return b.lastVisited() - a.lastVisited(); });
let docIdsToRemove = []; for (i = 0; i < Math.min(items.length, 5); i++)
for (docId in this._allItems) { this.addItem(items[i]);
// 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];
}
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));
}
},
_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();
} }
}; };