diff --git a/js/ui/appdisplay.js b/js/ui/appdisplay.js new file mode 100644 index 000000000..53eb8952f --- /dev/null +++ b/js/ui/appdisplay.js @@ -0,0 +1,216 @@ +/* -*- mode: js2; js2-basic-offset: 4; -*- */ + +const Signals = imports.signals; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const Pango = imports.gi.Pango; +const Gtk = imports.gi.Gtk; + +const Tidy = imports.gi.Tidy; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; + +const MENU_NAME_COLOR = new Clutter.Color(); +MENU_NAME_COLOR.from_pixel(0xffffffff); +const MENU_COMMENT_COLOR = new Clutter.Color(); +MENU_COMMENT_COLOR.from_pixel(0xffffffbb); +const MENU_BACKGROUND_COLOR = new Clutter.Color(); +MENU_BACKGROUND_COLOR.from_pixel(0x000000ff); + +const APPDISPLAY_HEIGHT = 50; +const APPDISPLAY_PADDING = 4; + +function AppDisplayItem(node, width) { + this._init(node, width); +} + +AppDisplayItem.prototype = { + _init: function(appinfo, width) { + let me = this; + this._appinfo = appinfo; + + let name = appinfo.get_name(); + + let icontheme = Gtk.icon_theme_get_default(); + + this._group = new Clutter.Group({reactive: true, + width: width, + height: APPDISPLAY_HEIGHT}); + this._bg = new Clutter.Rectangle({ color: MENU_BACKGROUND_COLOR, + reactive: true }); + this._group.add_actor(this._bg); + this._bg.connect('button-press-event', function(group, e) { + me.emit('launch'); + return true; + }); + + this._icon = new Clutter.Texture({ width: 48, height: 48 }); + let gicon = appinfo.get_icon(); + let path = null; + if (gicon != null) { + let iconinfo = icontheme.lookup_by_gicon(gicon, 48, Gtk.IconLookupFlags.NO_SVG); + if (iconinfo) + path = iconinfo.get_filename(); + } + + if (path) + this._icon.set_from_file(path); + this._group.add_actor(this._icon); + + let comment = appinfo.get_description(); + let text_width = width - me._icon.width + 4; + this._name = new Clutter.Label({ color: MENU_NAME_COLOR, + font_name: "Sans 14px", + width: text_width, + ellipsize: Pango.EllipsizeMode.END, + text: name}); + this._group.add_actor(this._name); + this._comment = new Clutter.Label({ color: MENU_COMMENT_COLOR, + font_name: "Sans 12px", + width: text_width, + ellipsize: Pango.EllipsizeMode.END, + text: comment}) + this._group.add_actor(this._comment); + + this._group.connect("notify::allocation", function (grp, prop) { + let x = me._group.x; + let y = me._group.y; + let width = me._group.width; + let height = me._group.height; + me._bg.set_position(x, y); + me._icon.set_position(x, y); + let text_x = x + me._icon.width + 4; + me._name.set_position(text_x, y); + me._comment.set_position(text_x, y + me._name.get_height() + 4); + }); + + this.actor = this._group; + } +} +Signals.addSignalMethods(AppDisplayItem.prototype); + +function AppDisplay(x, y, width, height) { + this._init(x, y, width, height); +} + +AppDisplay.prototype = { + _init : function(x, y, width, height) { + let me = this; + let global = Shell.global_get(); + this._x = x; + this._y = y; + this._width = width; + this._height = height; + this._appmonitor = new Shell.AppMonitor(); + this._appsStale = true; + this._appmonitor.connect('changed', function(mon) { + me._appsStale = true; + }); + this._grid = new Tidy.Grid({x: x, y: y, width: width, height: height, + column_major: true, + column_gap: APPDISPLAY_PADDING }); + global.stage.add_actor(this._grid); + this._appset = {}; // Map + this._displayed = {} // Map + this._max_items = this._height / (APPDISPLAY_HEIGHT + APPDISPLAY_PADDING); + }, + + _refresh: function() { + let me = this; + + if (!this._appsStale) + return; + for (id in this._displayed) + this._displayed[id].destroy(); + this._appset = {}; + this._displayed = {}; + let apps = Gio.app_info_get_all(); + let i = 0; + for (i = 0; i < apps.length; i++) { + let appinfo = apps[i]; + let appid = appinfo.get_id(); + this._appset[appid] = appinfo; + } + for (i = 0; i < apps.length && i < this._max_items; i++) { + let appinfo = apps[i]; + let appid = appinfo.get_id(); + this._filterAdd(appid); + } + this._appsStale = false; + }, + + _filterAdd: function(appid) { + let me = this; + + let appinfo = this._appset[appid]; + let name = appinfo.get_name(); + let index = 0; for (i in this._displayed) { index += 1; } + + let appdisplay = new AppDisplayItem(appinfo, this._width); + appdisplay.connect('launch', function() { + appinfo.launch([], null); + me.emit('activated'); + }); + let group = appdisplay.actor; + this._grid.add_actor(group); + this._displayed[appid] = appdisplay; + }, + + _filterRemove: function(appid) { + let item = this._displayed[appid]; + let group = item.actor; + group.destroy(); + delete this._displayed[appid]; + }, + + _appinfoMatches: function(appinfo, search) { + if (search == null || search == '') + return true; + let name = appinfo.get_name().toLowerCase(); + let description = appinfo.get_description(); + if (description) description = description.toLowerCase(); + if (name.indexOf(search) >= 0) + return true; + if (description && description.indexOf(search) >= 0) + return true; + return false; + }, + + _doSearchFilter: function() { + let c = 0; + for (appid in this._displayed) { + let app = this._appset[appid]; + if (!this._appinfoMatches(app, this._search)) + this._filterRemove(appid); + else + c += 1; + } + for (appid in this._appset) { + if (c >= this._max_items) + break; + if (appid in this._displayed) + continue; + let app = this._appset[appid]; + if (this._appinfoMatches(app, this._search)) { + this._filterAdd(appid); + c += 1; + } + } + }, + + setSearch: function(text) { + this._search = text.toLowerCase(); + this._doSearchFilter(); + }, + + show: function() { + this._refresh(); + this._grid.show(); + }, + + hide: function() { + this._grid.hide(); + } +} +Signals.addSignalMethods(AppDisplay.prototype); + diff --git a/js/ui/overlay.js b/js/ui/overlay.js index 3a17e5a97..a61bde84c 100644 --- a/js/ui/overlay.js +++ b/js/ui/overlay.js @@ -1,16 +1,28 @@ /* -*- mode: js2; js2-basic-offset: 4; -*- */ -const Clutter = imports.gi.Clutter; -const Meta = imports.gi.Meta; -const Shell = imports.gi.Shell; +const Signals = imports.signals; +const Mainloop = imports.mainloop; const Tweener = imports.tweener.tweener; +const Clutter = imports.gi.Clutter; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; const Main = imports.ui.main; const Panel = imports.ui.panel; +const Meta = imports.gi.Meta; +const Shell = imports.gi.Shell; +const AppDisplay = imports.ui.appdisplay; const OVERLAY_BACKGROUND_COLOR = new Clutter.Color(); OVERLAY_BACKGROUND_COLOR.from_pixel(0x000000ff); +const SIDESHOW_PAD = 6; +const SIDESHOW_MIN_WIDTH = 250; +const SIDESHOW_SEARCH_BG_COLOR = new Clutter.Color(); +SIDESHOW_SEARCH_BG_COLOR.from_pixel(0xffffffff); +const SIDESHOW_TEXT_COLOR = new Clutter.Color(); +SIDESHOW_TEXT_COLOR.from_pixel(0xffffffff); + // Time for initial animation going into overlay mode const ANIMATION_TIME = 0.3; @@ -33,12 +45,90 @@ const POSITIONS = { 4: [[0.25, 0.25, 0.3], [0.75, 0.25, 0.3], [0.75, 0.75, 0.3], [0.25, 0.75, 0.3]] }; +function Sideshow(width) { + this._init(width); +} + +Sideshow.prototype = { + _init : function(width) { + let me = this; + + let global = Shell.global_get(); + this._group = new Clutter.Group(); + this._group.hide(); + global.stage.add_actor(this._group); + let icontheme = Gtk.icon_theme_get_default(); + let rect = new Clutter.Rectangle({ color: SIDESHOW_SEARCH_BG_COLOR, + x: SIDESHOW_PAD, + y: Panel.PANEL_HEIGHT + SIDESHOW_PAD, + width: width, + height: 24}); + this._group.add_actor(rect); + + let searchIconTexture = new Clutter.Texture({ x: SIDESHOW_PAD + 2, + y: rect.y + 2 }); + let searchIconPath = icontheme.lookup_icon('gtk-find', 16, 0).get_filename(); + searchIconTexture.set_from_file(searchIconPath); + this._group.add_actor(searchIconTexture); + + this._searchEntry = new Clutter.Entry({ + font_name: "Sans 14px", + x: searchIconTexture.x + + searchIconTexture.width + 4, + y: searchIconTexture.y, + width: rect.width - (searchIconTexture.x), + height: searchIconTexture.height}); + this._group.add_actor(this._searchEntry); + global.stage.set_key_focus(this._searchEntry); + this._searchQueued = false; + this._searchEntry.connect('notify::text', function (se, prop) { + if (me._searchQueued) + return; + Mainloop.timeout_add(250, function() { + me._searchQueued = false; + me._appdisplay.setSearch(me._searchEntry.text); + return false; + }); + }); + + let appsText = new Clutter.Label({ color: SIDESHOW_TEXT_COLOR, + font_name: "Sans Bold 14px", + text: "Applications", + x: SIDESHOW_PAD, + y: this._searchEntry.y + this._searchEntry.height + 10, + height: 16}); + this._group.add_actor(appsText); + + let menuY = appsText.y + appsText.height + 6; + this._appdisplay = new AppDisplay.AppDisplay(SIDESHOW_PAD, + menuY, width, global.screen_height - menuY); + + /* Proxy the activated signal */ + this._appdisplay.connect('activated', function(appdisplay) { + me.emit('activated'); + }); + }, + + show: function() { + this._group.show(); + this._appdisplay.show(); + }, + + hide: function() { + this._group.hide(); + this._appdisplay.hide(); + } +}; +Signals.addSignalMethods(Sideshow.prototype); + function Overlay() { this._init(); -}; +} Overlay.prototype = { _init : function() { + let me = this; + let global = Shell.global_get(); this._group = new Clutter.Group(); @@ -56,6 +146,29 @@ Overlay.prototype = { global.overlay_group.add_actor(this._group); this._windowClones = [] + + // TODO - recalculate everything when desktop size changes + this._recalculateSize(); + + this._sideshow = new Sideshow(this._desktopX - 10); + this._sideshow.connect('activated', function(sideshow) { + // TODO - have some sort of animation/effect while + // transitioning to the new app. We definitely need + // startup-notification integration at least. + me.hide(); + }); + }, + + _recalculateSize: function () { + let global = Shell.global_get(); + let screenWidth = global.screen_width; + let screenHeight = global.screen_height; + // The desktop windows are shown on top of a scaled down version of the + // desktop. This is positioned at the right side of the screen + this._desktopWidth = screenWidth * DESKTOP_SCALE; + this._desktopHeight = screenHeight * DESKTOP_SCALE; + this._desktopX = screenWidth - this._desktopWidth - 10; + this._desktopY = Panel.PANEL_HEIGHT + (screenHeight - this._desktopHeight - Panel.PANEL_HEIGHT) / 2; }, show : function() { @@ -63,23 +176,18 @@ Overlay.prototype = { this.visible = true; let global = Shell.global_get(); + + global.focus_stage(); + let windows = global.get_windows(); let desktopWindow = null; - let screenWidth = global.screen_width - let screenHeight = global.screen_height + this._recalculateSize(); for (let i = 0; i < windows.length; i++) if (windows[i].get_window_type() == Meta.WindowType.DESKTOP) desktopWindow = windows[i]; - // The desktop windows are shown on top of a scaled down version of the - // desktop. This is positioned at the right side of the screen - this._desktopWidth = screenWidth * DESKTOP_SCALE; - this._desktopHeight = screenHeight * DESKTOP_SCALE; - this._desktopX = screenWidth - this._desktopWidth - 10; - this._desktopY = Panel.PANEL_HEIGHT + (screenHeight - this._desktopHeight - Panel.PANEL_HEIGHT) / 2; - // If a file manager is displaying desktop icons, there will be a desktop window. // This window will have the size of the whole desktop. When such window is not present // (e.g. when the preference for showing icons on the desktop is disabled by the user @@ -112,6 +220,8 @@ Overlay.prototype = { windowIndex++; } + this._sideshow.show(); + // All the the actors in the window group are completely obscured, // hiding the group holding them while the overlay is displayed greatly // increases performance of the overlay especially when there are many @@ -136,6 +246,8 @@ Overlay.prototype = { this._windowClones[i].destroy(); } + this._sideshow.hide(); + this._windowClones = []; } }, @@ -166,6 +278,8 @@ Overlay.prototype = { }, _addDesktop : function(desktop) { + let me = this; + this._windowClones.push(desktop); this._group.add_actor(desktop); @@ -178,7 +292,6 @@ Overlay.prototype = { transition: "linear" }); - let me = this; desktop.connect("button-press-event", function() { me._deactivate(); @@ -205,6 +318,8 @@ Overlay.prototype = { }, _createWindowClone : function(w, windowIndex, numberOfWindows) { + let me = this; + // We show the window using "clones" of the texture .. separate // actors that mirror the original actors for the window. For // animation purposes, it may be better to actually move the @@ -244,7 +359,6 @@ Overlay.prototype = { transition: "linear" }); - let me = this; clone.connect("button-press-event", function(clone, event) { me._activateWindow(w, event.get_time()); diff --git a/src/Makefile.am b/src/Makefile.am index 3ec54d4ad..3f0a53dad 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -15,6 +15,8 @@ plugin_LTLIBRARIES = libgnome-shell.la libgnome_shell_la_SOURCES = \ gnome-shell-plugin.c \ + shell-app-monitor.c \ + shell-app-monitor.h \ shell-process.c \ shell-process.h \ shell-global.c \ diff --git a/src/shell-app-monitor.c b/src/shell-app-monitor.c new file mode 100644 index 000000000..cbe19281f --- /dev/null +++ b/src/shell-app-monitor.c @@ -0,0 +1,95 @@ +/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */ + +#include "shell-app-monitor.h" + +#include + +enum { + PROP_0, + +}; + +enum { + CHANGED, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +struct _ShellAppMonitorPrivate { + GList *desktop_dir_monitors; +}; + +static void shell_app_monitor_finalize (GObject *object); +static void on_monitor_changed (GFileMonitor *monitor, GFile *file, + GFile *other_file, GFileMonitorEvent event_type, + gpointer user_data); + +G_DEFINE_TYPE(ShellAppMonitor, shell_app_monitor, G_TYPE_OBJECT); + +static void shell_app_monitor_class_init(ShellAppMonitorClass *klass) +{ + GObjectClass *gobject_class = (GObjectClass *)klass; + + gobject_class->finalize = shell_app_monitor_finalize; + + signals[CHANGED] = + g_signal_new ("changed", + SHELL_TYPE_APP_MONITOR, + G_SIGNAL_RUN_LAST, + G_STRUCT_OFFSET (ShellAppMonitorClass, changed), + NULL, NULL, + g_cclosure_marshal_VOID__VOID, + G_TYPE_NONE, 0); + + g_type_class_add_private (gobject_class, sizeof (ShellAppMonitorPrivate)); +} + +static void +shell_app_monitor_init (ShellAppMonitor *self) +{ + const gchar *const *iter; + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + SHELL_TYPE_APP_MONITOR, + ShellAppMonitorPrivate); + for (iter = g_get_system_data_dirs (); *iter; iter++) + { + GFile *dir; + GFileMonitor *monitor; + GError *error = NULL; + + dir = g_file_new_for_path (*iter); + monitor = g_file_monitor_directory (dir, 0, NULL, &error); + if (!monitor) { + g_warning ("failed to monitor %s", error->message); + g_clear_error (&error); + continue; + } + g_signal_connect (monitor, "changed", G_CALLBACK (on_monitor_changed), self); + self->priv->desktop_dir_monitors + = g_list_prepend (self->priv->desktop_dir_monitors, + monitor); + g_object_unref (dir); + } +} + +static void +shell_app_monitor_finalize (GObject *object) +{ + ShellAppMonitor *self = SHELL_APP_MONITOR (object); + + g_list_foreach (self->priv->desktop_dir_monitors, (GFunc) g_object_unref, NULL); + g_list_free (self->priv->desktop_dir_monitors); + + G_OBJECT_CLASS (shell_app_monitor_parent_class)->finalize(object); +} + +static void +on_monitor_changed (GFileMonitor *monitor, GFile *file, + GFile *other_file, GFileMonitorEvent event_type, + gpointer user_data) +{ + ShellAppMonitor *self = SHELL_APP_MONITOR (user_data); + + g_signal_emit (self, signals[CHANGED], 0); +} diff --git a/src/shell-app-monitor.h b/src/shell-app-monitor.h new file mode 100644 index 000000000..e5d8923cb --- /dev/null +++ b/src/shell-app-monitor.h @@ -0,0 +1,34 @@ +#ifndef __SHELL_APP_MONITOR_H__ +#define __SHELL_APP_MONITOR_H__ + +#include + +#define SHELL_TYPE_APP_MONITOR (shell_app_monitor_get_type ()) +#define SHELL_APP_MONITOR(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), SHELL_TYPE_APP_MONITOR, ShellAppMonitor)) +#define SHELL_APP_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass)) +#define SHELL_IS_APP_MONITOR(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), SHELL_TYPE_APP_MONITOR)) +#define SHELL_IS_APP_MONITOR_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), SHELL_TYPE_APP_MONITOR)) +#define SHELL_APP_MONITOR_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), SHELL_TYPE_APP_MONITOR, ShellAppMonitorClass)) + +typedef struct _ShellAppMonitor ShellAppMonitor; +typedef struct _ShellAppMonitorClass ShellAppMonitorClass; +typedef struct _ShellAppMonitorPrivate ShellAppMonitorPrivate; + +struct _ShellAppMonitor +{ + GObject parent; + + ShellAppMonitorPrivate *priv; +}; + +struct _ShellAppMonitorClass +{ + GObjectClass parent_class; + + void (*changed)(ShellAppMonitor *menuwrapper, gpointer data); +}; + +GType shell_app_monitor_get_type (void) G_GNUC_CONST; +ShellAppMonitor* shell_app_monitor_new(void); + +#endif /* __SHELL_APP_MONITOR_H__ */