[gnome-shell] Add an initial implementation of the sidebar



commit 10afe4619505d159f6639e70a346902082671ff1
Author: Dan Winship <danw gnome org>
Date:   Fri Apr 24 10:01:34 2009 -0400

    Add an initial implementation of the sidebar
    
    This still needs design love, and none of the current widgets should be
    considered finalized, but it shows the basic ideas.
    
    http://bugzilla.gnome.org/show_bug.cgi?id=581774
---
 js/ui/Makefile.am  |    3 +
 js/ui/main.js      |    3 +
 js/ui/sidebar.js   |  153 ++++++++++++++++++++++
 js/ui/widget.js    |  293 ++++++++++++++++++++++++++++++++++++++++++
 js/ui/widgetBox.js |  359 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/shell-global.c |   60 +++++++++
 src/shell-global.h |    6 +
 7 files changed, 877 insertions(+), 0 deletions(-)

diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am
index 8465b9e..e344982 100644
--- a/js/ui/Makefile.am
+++ b/js/ui/Makefile.am
@@ -14,6 +14,9 @@ dist_jsui_DATA =		\
 	overlay.js		\
 	panel.js		\
 	runDialog.js		\
+	sidebar.js		\
 	tweener.js		\
+	widget.js		\
+	widgetBox.js		\
 	windowManager.js	\
 	workspaces.js
diff --git a/js/ui/main.js b/js/ui/main.js
index 6886f8a..a9d8f07 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -13,6 +13,7 @@ const Chrome = imports.ui.chrome;
 const Overlay = imports.ui.overlay;
 const Panel = imports.ui.panel;
 const RunDialog = imports.ui.runDialog;
+const Sidebar = imports.ui.sidebar;
 const Tweener = imports.ui.tweener;
 const WindowManager = imports.ui.windowManager;
 
@@ -21,6 +22,7 @@ DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff);
 
 let chrome = null;
 let panel = null;
+let sidebar = null;
 let overlay = null;
 let runDialog = null;
 let wm = null;
@@ -66,6 +68,7 @@ function start() {
     overlay = new Overlay.Overlay();
     chrome = new Chrome.Chrome();
     panel = new Panel.Panel();
+    sidebar = new Sidebar.Sidebar();
     wm = new WindowManager.WindowManager();
     
     global.screen.connect('toggle-recording', function() {
diff --git a/js/ui/sidebar.js b/js/ui/sidebar.js
new file mode 100644
index 0000000..0afd8d8
--- /dev/null
+++ b/js/ui/sidebar.js
@@ -0,0 +1,153 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Big = imports.gi.Big;
+const Clutter = imports.gi.Clutter;
+const Shell = imports.gi.Shell;
+const Lang = imports.lang;
+
+const Main = imports.ui.main;
+const Panel = imports.ui.panel;
+const Tweener = imports.ui.tweener;
+const Widget = imports.ui.widget;
+const WidgetBox = imports.ui.widgetBox;
+
+const SIDEBAR_SPACING = 4;
+const SIDEBAR_PADDING = 4;
+
+// The total sidebar width is the widget width plus the widget
+// padding, plus the sidebar padding
+const SIDEBAR_COLLAPSED_WIDTH = Widget.COLLAPSED_WIDTH + 2 * WidgetBox.WIDGETBOX_PADDING + 2 * SIDEBAR_PADDING;
+const SIDEBAR_EXPANDED_WIDTH = Widget.EXPANDED_WIDTH + 2 * WidgetBox.WIDGETBOX_PADDING + 2 * SIDEBAR_PADDING;
+
+// The maximum height of the sidebar would be extending from just
+// below the panel to just above the taskbar. Since the taskbar is
+// just a temporary hack and it would be too hard to do this the right
+// way, we just hardcode its size.
+const HARDCODED_TASKBAR_HEIGHT = 24;
+const MAXIMUM_SIDEBAR_HEIGHT = Shell.Global.get().screen_height - Panel.PANEL_HEIGHT - HARDCODED_TASKBAR_HEIGHT;
+
+// FIXME, needs to be configurable, obviously
+const default_widgets = [
+    "imports.ui.widget.ClockWidget",
+    "imports.ui.widget.AppsWidget",
+    "imports.ui.widget.DocsWidget"
+];
+
+function Sidebar() {
+    this._init();
+}
+
+Sidebar.prototype = {
+    _init : function() {
+        let global = Shell.Global.get();
+
+        // The top-left corner of the sidebar is fixed at:
+        // x = -WidgetBox.WIDGETBOX_PADDING, y = Panel.PANEL_HEIGHT.
+        // (The negative X is so that we don't see the rounded
+        // WidgetBox corners on the screen edge side.)
+        this.actor = new Clutter.Group({ x: -WidgetBox.WIDGETBOX_PADDING,
+                                         y: Panel.PANEL_HEIGHT,
+                                         width: SIDEBAR_EXPANDED_WIDTH });
+        Main.chrome.addActor(this.actor);
+
+        // The actual widgets go into a Big.Box inside this.actor. The
+        // box's width will vary during the expand/collapse animations,
+        // but this.actor's width will remain constant until we adjust
+        // it at the end of the animation, because we don't want the
+        // wm strut to move and cause windows to move multiple times
+        // during the animation.
+        this.box = new Big.Box ({ padding_top: SIDEBAR_PADDING,
+                                  padding_bottom: SIDEBAR_PADDING,
+                                  padding_right: SIDEBAR_PADDING,
+                                  padding_left: 0,
+                                  spacing: SIDEBAR_SPACING });
+        this.actor.add_actor(this.box);
+
+        this._visible = this.expanded = true;
+
+        this._widgets = [];
+        this.addWidget(new ToggleWidget(this));
+        for (let i = 0; i < default_widgets.length; i++)
+            this.addWidget(default_widgets[i]);
+    },
+
+    addWidget: function(widget) {
+        let widgetBox;
+        try {
+            widgetBox = new WidgetBox.WidgetBox(widget);
+        } catch(e) {
+            logError(e, "Failed to add widget '" + widget + "'");
+            return;
+        }
+
+        this.box.append(widgetBox.actor, Big.BoxPackFlags.NONE);
+        this._widgets.push(widgetBox);
+    },
+
+    show: function() {
+        this._visible = true;
+        this.actor.show();
+    },
+
+    hide: function() {
+        this._visible = false;
+        this.actor.hide();
+    },
+
+    expand: function() {
+        this.expanded = true;
+        for (let i = 0; i < this._widgets.length; i++)
+            this._widgets[i].expand();
+
+        // Updated the strut/stage area after the animation completes
+        Tweener.addTween(this, { time: WidgetBox.ANIMATION_TIME,
+                                 onComplete: function () {
+                                     this.actor.width = SIDEBAR_EXPANDED_WIDTH;
+                                 } });
+    },
+
+    collapse: function() {
+        this.expanded = false;
+        for (let i = 0; i < this._widgets.length; i++)
+            this._widgets[i].collapse();
+
+        // Updated the strut/stage area after the animation completes
+        Tweener.addTween(this, { time: WidgetBox.ANIMATION_TIME,
+                                 onComplete: function () {
+                                     this.actor.width = SIDEBAR_COLLAPSED_WIDTH;
+                                 } });
+    },
+
+    destroy: function() {
+        this.hide();
+
+        for (let i = 0; i < this._widgets.length; i++)
+            this._widgets[i].destroy();
+        this.actor.destroy();
+    }
+};
+
+const LEFT_DOUBLE_ARROW = "\u00AB";
+const RIGHT_DOUBLE_ARROW = "\u00BB";
+
+function ToggleWidget(sidebar) {
+    this._init(sidebar);
+}
+
+ToggleWidget.prototype = {
+    __proto__ : Widget.Widget.prototype,
+
+    _init : function(sidebar) {
+        this._sidebar = sidebar;
+        this.actor = new Clutter.Text({ font_name: "Sans Bold 16px",
+                                        text: LEFT_DOUBLE_ARROW,
+                                        reactive: true });
+        this.actor.connect('button-release-event',
+                           Lang.bind(this._sidebar, this._sidebar.collapse));
+        this.collapsedActor = new Clutter.Text({ font_name: "Sans Bold 16px",
+                                                 text: RIGHT_DOUBLE_ARROW,
+                                                 reactive: true });
+        this.collapsedActor.connect('button-release-event',
+                                    Lang.bind(this._sidebar, this._sidebar.expand));
+    }
+};
diff --git a/js/ui/widget.js b/js/ui/widget.js
new file mode 100644
index 0000000..2e38bf0
--- /dev/null
+++ b/js/ui/widget.js
@@ -0,0 +1,293 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Big = imports.gi.Big;
+const Clutter = imports.gi.Clutter;
+const Gio = imports.gi.Gio;
+const Gtk = imports.gi.Gtk;
+const Mainloop = imports.mainloop;
+const Lang = imports.lang;
+const Shell = imports.gi.Shell;
+const Signals = imports.signals;
+
+const AppDisplay = imports.ui.appDisplay;
+const DocDisplay = imports.ui.docDisplay;
+
+const COLLAPSED_WIDTH = 24;
+const EXPANDED_WIDTH = 200;
+
+const STATE_EXPANDED    = 0;
+const STATE_COLLAPSING  = 1;
+const STATE_COLLAPSED   = 2;
+const STATE_EXPANDING   = 3;
+const STATE_POPPING_OUT = 4;
+const STATE_POPPED_OUT  = 5;
+const STATE_POPPING_IN  = 6;
+
+function Widget() {
+}
+
+Widget.prototype = {
+    // _init():
+    //
+    // Your widget constructor. Receives no arguments. Must define a
+    // field named "actor" containing the Clutter.Actor to show in
+    // expanded mode. This actor will be clipped to
+    // Widget.EXPANDED_WIDTH. Most widgets will also define a field
+    // named "title" containing the title string to show above the
+    // widget in the sidebar.
+    //
+    // If you want to have a separate collapsed view, you can define a
+    // field "collapsedActor" containing the Clutter.Actor to show in
+    // that mode. (It may be the same actor.) This actor will be
+    // clipped to Widget.COLLAPSED_WIDTH, and will normally end up
+    // having the same height as the main actor.
+    //
+    // If you do not set a collapsedActor, then you must set a title,
+    // since that is what will be displayed in collapsed mode, and
+    // in this case (and only in this case), the widget will support
+    // pop-out, meaning that if the user hovers over its title while
+    // the sidebar is collapsed, the widget's expanded view will pop
+    // out of the sidebar until either the cursor moves out of it,
+    // or else the widget calls this.activated() on itself.
+
+    // destroy():
+    //
+    // Optional. Will be called when the widget is removed from the
+    // sidebar. (Note that you don't need to destroy the actors,
+    // since they will be destroyed for you.)
+
+    // collapse():
+    //
+    // Optional. Called during the sidebar collapse process, at the
+    // point when the expanded sidebar has slid offscreen, but the
+    // collapsed sidebar has not yet slid onscreen.
+
+    // expand():
+    //
+    // Optional. Called during the sidebar expand process, at the
+    // point when the collapsed sidebar has slid offscreen, but the
+    // expanded sidebar has not yet slid onscreen.
+
+    // activated():
+    //
+    // Emits the "activated" signal for you, which will cause pop-out
+    // to end.
+    activated: function() {
+        this.emit('activated');
+    }
+
+    // state:
+    //
+    // A field set on your widget by the sidebar. Will contain one of
+    // the Widget.STATE_* values. (Eg, Widget.STATE_EXPANDED). Note
+    // that this will not be set until *after* _init() is called, so
+    // you cannot rely on it being set at that point. The widget will
+    // always initially be in STATE_EXPANDED.
+};
+
+Signals.addSignalMethods(Widget.prototype);
+
+
+function ClockWidget() {
+  this._init();
+}
+
+ClockWidget.prototype = {
+    __proto__ : Widget.prototype,
+
+    _init: function() {
+        this.actor = new Clutter.Text({ font_name: "Sans Bold 16px",
+                                        text: "",
+                                        // Give an explicit height to ensure
+                                        // it's the same in both modes
+                                        height: COLLAPSED_WIDTH });
+
+        this.collapsedActor = new Clutter.CairoTexture({ width: COLLAPSED_WIDTH,
+                                                         height: COLLAPSED_WIDTH,
+                                                         surface_width: COLLAPSED_WIDTH,
+                                                         surface_height: COLLAPSED_WIDTH });
+
+        this._update();
+    },
+
+    destroy: function() {
+        if (this.timer)
+            Mainloop.source_remove(this.timer);
+    },
+
+    expand: function() {
+        this._update();
+    },
+
+    collapse: function() {
+        this._update();
+    },
+
+    _update: function() {
+        let time = new Date();
+        let msec_remaining = 60000 - (1000 * time.getSeconds() +
+                                      time.getMilliseconds());
+        if (msec_remaining < 500) {
+            time.setMinutes(time.getMinutes() + 1);
+            msec_remaining += 60000;
+        }
+
+        if (this.state == STATE_COLLAPSED || this.state == STATE_COLLAPSING)
+            this._updateCairo(time);
+        else
+            this._updateText(time);
+
+        if (this.timer)
+            Mainloop.source_remove(this.timer);
+        this.timer = Mainloop.timeout_add(msec_remaining, Lang.bind(this, this._update));
+        return false;
+    },
+
+    _updateText: function(time) {
+        this.actor.set_text(time.toLocaleFormat("%H:%M"));
+    },
+
+    _updateCairo: function(time) {
+        let global = Shell.Global.get();
+        global.clutter_cairo_texture_draw_clock(this.collapsedActor,
+                                                time.getHours() % 12,
+                                                time.getMinutes());
+    }
+};
+
+
+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 AppsWidget() {
+    this._init();
+}
+
+AppsWidget.prototype = {
+    __proto__ : Widget.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();
+    }
+};
+
+function DocsWidget() {
+    this._init();
+}
+
+DocsWidget.prototype = {
+    __proto__ : Widget.prototype,
+
+    _init : function() {
+        this.title = "Recent Docs";
+        this.actor = new Big.Box({ spacing: 2 });
+
+        this._recentManager = Gtk.RecentManager.get_default();
+        this._recentManager.connect('changed', Lang.bind(this, this._recentChanged));
+        this._recentChanged();
+    },
+
+    _recentChanged: function() {
+        let i, docId;
+
+        this._allItems = {};
+        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];
+        }
+
+        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();
+    }
+};
diff --git a/js/ui/widgetBox.js b/js/ui/widgetBox.js
new file mode 100644
index 0000000..78baf86
--- /dev/null
+++ b/js/ui/widgetBox.js
@@ -0,0 +1,359 @@
+/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
+
+const Big = imports.gi.Big;
+const Clutter = imports.gi.Clutter;
+const Shell = imports.gi.Shell;
+const Lang = imports.lang;
+const Mainloop = imports.mainloop;
+
+const Main = imports.ui.main;
+const Tweener = imports.ui.tweener;
+const Widget = imports.ui.widget;
+
+const WIDGETBOX_BG_COLOR = new Clutter.Color();
+WIDGETBOX_BG_COLOR.from_pixel(0xf0f0f0ff);
+const BLACK = new Clutter.Color();
+BLACK.from_pixel(0x000000ff);
+
+const WIDGETBOX_PADDING = 4;
+const ANIMATION_TIME = 0.5;
+const POP_IN_LAG = 250; /* milliseconds */
+
+function WidgetBox(widget) {
+    this._init(widget);
+}
+
+WidgetBox.prototype = {
+    _init: function(widget) {
+	if (widget instanceof Widget.Widget)
+	    this._widget = widget;
+	else {
+	    let ctor = this._ctorFromName(widget);
+            this._widget = new ctor();
+	}
+
+        if (!this._widget.actor)
+            throw new Error("widget has no actor");
+        else if (!this._widget.title && !this._widget.collapsedActor)
+            throw new Error("widget has neither title nor collapsedActor");
+
+        this.state = this._widget.state = Widget.STATE_EXPANDED;
+
+        // The structure of a WidgetBox:
+        //
+        // The top level is a Clutter.Group, which exists to make
+        // pop-out work correctly; when another widget pops out, its
+        // width will increase, which will in turn cause the sidebar's
+        // width to increase, which will cause the sidebar to increase
+        // the width of each of its children (the WidgetBoxes). But we
+        // don't want the non-popped-out widgets to expand, so we make
+        // the top-level actor be a Clutter.Group, which will accept
+        // the new width from the Sidebar, but not impose it on its
+        // own child.
+        //
+        // Inside the toplevel group is a horizontal Big.Box
+        // containing 2 Clutter.Groups; one for the collapsed state
+        // (cgroup) and one for the expanded state (egroup). Each
+        // group contains a single vertical Big.Box (cbox and ebox
+        // respectively), which have the appropriate fixed width. The
+        // cbox contains either the collapsed widget actor or else the
+        // rotated title. The ebox contains the horizontal title (if
+        // any), separator line, and the expanded widget actor. (If
+        // the widget doesn't have a collapsed actor, and therefore
+        // supports pop-out, then it will also have a vertical line
+        // between the two groups, which will only be shown during
+        // pop-out.)
+        //
+        // In the expanded view, cgroup is hidden and egroup is shown.
+        // When animating to the collapsed view, first the ebox is
+        // slid offscreen by giving it increasingly negative x
+        // coordinates within egroup. Then once it's fully offscreen,
+        // we hide egroup, show cgroup, and slide cbox back in in the
+        // same way.
+        //
+        // The pop-out view works similarly to the second half of the
+        // collapsed-to-expanded transition, except that the
+        // horizontal title gets hidden to avoid duplication.
+
+        this.actor = new Clutter.Group();
+        this._hbox = new Big.Box({ background_color: WIDGETBOX_BG_COLOR,
+                                   padding: WIDGETBOX_PADDING,
+                                   spacing: WIDGETBOX_PADDING,
+                                   corner_radius: WIDGETBOX_PADDING / 2,
+                                   orientation: Big.BoxOrientation.HORIZONTAL,
+                                   reactive: true });
+        this.actor.add_actor(this._hbox);
+
+        this._cgroup = new Clutter.Group({ clip_to_allocation: true });
+        this._hbox.append(this._cgroup, Big.BoxPackFlags.NONE);
+
+        this._cbox = new Big.Box({ width: Widget.COLLAPSED_WIDTH,
+                                   clip_to_allocation: true });
+        this._cgroup.add_actor(this._cbox);
+
+        if (this._widget.collapsedActor) {
+            if (this._widget.collapsedActor == this._widget.actor)
+                this._singleActor = true;
+            else {
+                this._cbox.append(this._widget.collapsedActor,
+                                  Big.BoxPackFlags.NONE);
+            }
+        } else {
+            let vtitle = new Clutter.Text({ font_name: "Sans 16px",
+                                            text: this._widget.title,
+                                            rotation_angle_z: -90.0 });
+            let signalId = vtitle.connect('notify::allocation',
+                                          function () {
+                                              vtitle.disconnect(signalId);
+                                              vtitle.set_anchor_point(vtitle.natural_width, 0);
+                                              vtitle.set_size(vtitle.natural_height,
+                                                              vtitle.natural_width);
+                                          });
+            this._vtitle = vtitle;
+            this._cbox.append(this._vtitle, Big.BoxPackFlags.NONE);
+
+            this._vline = new Clutter.Rectangle({ color: BLACK, width: 1 });
+            this._hbox.append(this._vline, Big.BoxPackFlags.NONE);
+            this._vline.hide();
+
+            // Set up pop-out
+            this._eventHandler = this._hbox.connect('captured-event',
+                                                    Lang.bind(this, this._popEventHandler));
+            this._activationHandler = this._widget.connect('activated',
+                                                           Lang.bind(this, this._activationHandler));
+        }
+        this._cgroup.hide();
+
+        this._egroup = new Clutter.Group({ clip_to_allocation: true });
+        this._hbox.append(this._egroup, Big.BoxPackFlags.NONE);
+
+        this._ebox = new Big.Box({ spacing: WIDGETBOX_PADDING,
+                                   width: Widget.EXPANDED_WIDTH,
+                                   clip_to_allocation: true });
+        this._egroup.add_actor(this._ebox);
+
+        if (this._widget.title) {
+            this._htitle = new Clutter.Text({ font_name: "Sans 16px",
+                                              text: this._widget.title });
+            this._ebox.append(this._htitle, Big.BoxPackFlags.NONE);
+
+            this._hline = new Clutter.Rectangle({ color: BLACK, height: 1 });
+            this._ebox.append(this._hline, Big.BoxPackFlags.NONE);
+        }
+
+        this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);
+    },
+
+    // Given a name like "imports.ui.widget.ClockWidget", turn that
+    // into a constructor function
+    _ctorFromName: function(name) {
+        // Make sure it's a valid import
+        if (!name.match(/^imports(\.[a-zA-Z0-9_]+)+$/))
+            throw new Error("widget name must start with 'imports.'");
+        if (name.match(/^imports\.gi\./))
+            throw new Error("cannot import widget from GIR");
+
+        let ctor = eval(name);
+
+        // Make sure it's really a constructor
+        if (!ctor || typeof(ctor) != "function")
+            throw new Error("widget name is not a constructor");
+
+        // Make sure it's a widget
+        let proto = ctor.prototype;
+        while (proto && proto != Widget.Widget.prototype)
+            proto = proto.__proto__;
+        if (!proto)
+            throw new Error("widget does not inherit from Widget prototype");
+
+        return ctor;
+    },
+
+    expand: function() {
+        Tweener.addTween(this._cbox, { x: -Widget.COLLAPSED_WIDTH,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._expandPart1Complete,
+                                       onCompleteScope: this });
+        this.state = this._widget.state = Widget.STATE_EXPANDING;
+    },
+
+    _expandPart1Complete: function() {
+        this._cgroup.hide();
+        this._cbox.x = 0;
+
+        if (this._singleActor) {
+            log(this._widget.actor);
+            this._widget.actor.unparent();
+            this._ebox.append(this._widget.actor, Big.BoxPackFlags.NONE);
+        }
+
+        if (this._widget.expand) {
+            try {
+                this._widget.expand();
+            } catch (e) {
+                logError(e, 'Widget failed to expand');
+            }
+        }
+
+        this._egroup.show();
+        if (this._htitle) {
+            this._htitle.show();
+            this._hline.show();
+        }
+        this._ebox.x = -Widget.EXPANDED_WIDTH;
+        Tweener.addTween(this._ebox, { x: 0,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._expandComplete,
+                                       onCompleteScope: this });
+    },
+
+    _expandComplete: function() {
+        this.state = this._widget.state = Widget.STATE_EXPANDED;
+    },
+
+    collapse: function() {
+        Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._collapsePart1Complete,
+                                       onCompleteScope: this });
+        this.state = this._widget.state = Widget.STATE_COLLAPSING;
+    },
+
+    _collapsePart1Complete: function() {
+        this._egroup.hide();
+        this._ebox.x = 0;
+        if (this._htitle) {
+            this._htitle.hide();
+            this._hline.hide();
+        }
+
+        if (this._singleActor) {
+            log(this._widget.actor);
+            this._widget.actor.unparent();
+            this._cbox.append(this._widget.actor, Big.BoxPackFlags.NONE);
+        }
+
+        if (this._widget.collapse) {
+            try {
+                this._widget.collapse();
+            } catch (e) {
+                logError(e, 'Widget failed to collapse');
+            }
+        }
+
+        this._cgroup.show();
+        this._cbox.x = -Widget.COLLAPSED_WIDTH;
+        if (this._vtitle)
+            this._cbox.height = this._ebox.height;
+        Tweener.addTween(this._cbox, { x: 0,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._collapseComplete,
+                                       onCompleteScope: this });
+    },
+
+    _collapseComplete: function() {
+        this.state = this._widget.state = Widget.STATE_COLLAPSED;
+    },
+
+    _popEventHandler: function(actor, event) {
+        let type = event.type();
+
+        if (type == Clutter.EventType.ENTER) {
+            this._clearPopInTimeout();
+            if (this.state == Widget.STATE_COLLAPSED ||
+                this.state == Widget.STATE_COLLAPSING) {
+                this._popOut();
+                return false;
+            }
+        } else if (type == Clutter.EventType.LEAVE &&
+                   (this.state == Widget.STATE_POPPED_OUT ||
+                    this.state == Widget.STATE_POPPING_OUT)) {
+            // If moving into another actor within this._hbox, let the
+            // event be propagated
+            let into = Shell.get_event_related(event);
+            while (into) {
+                if (into == this._hbox)
+                    return false;
+                into = into.get_parent();
+            }
+
+            // Else, moving out of this._hbox
+            this._setPopInTimeout();
+            return false;
+        }
+
+        return false;
+    },
+
+    _activationHandler: function() {
+        if (this.state == Widget.STATE_POPPED_OUT)
+            this._popIn();
+    },
+
+    _popOut: function() {
+        if (this.state != Widget.STATE_COLLAPSED &&
+            this.state != Widget.STATE_COLLAPSING)
+            return;
+
+        this._vline.show();
+        this._egroup.show();
+        this._ebox.x = -Widget.EXPANDED_WIDTH;
+        Tweener.addTween(this._ebox, { x: 0,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._popOutComplete,
+                                       onCompleteScope: this });
+        this.state = this._widget.state = Widget.STATE_POPPING_OUT;
+
+        Main.chrome.addInputRegionActor(this._hbox);
+    },
+
+    _popOutComplete: function() {
+        this.state = this._widget.state = Widget.STATE_POPPED_OUT;
+    },
+
+    _setPopInTimeout: function() {
+        this._clearPopInTimeout();
+        this._popInTimeout = Mainloop.timeout_add(POP_IN_LAG, Lang.bind(this, function () { this._popIn(); return false; }));
+    },
+
+    _clearPopInTimeout: function() {
+        if (this._popInTimeout) {
+            Mainloop.source_remove(this._popInTimeout);
+            delete this._popInTimeout;
+        }
+    },
+
+    _popIn: function() {
+        this._clearPopInTimeout();
+
+        if (this.state != Widget.STATE_POPPED_OUT &&
+            this.state != Widget.STATE_POPPING_OUT)
+            return;
+
+        Tweener.addTween(this._ebox, { x: -Widget.EXPANDED_WIDTH,
+                                       time: ANIMATION_TIME / 2,
+                                       transition: "easeOutQuad",
+                                       onComplete: this._popInComplete,
+                                       onCompleteScope: this });
+    },
+
+    _popInComplete: function() {
+        this.state = this._widget.state = Widget.STATE_COLLAPSED;
+        this._vline.hide();
+        this._egroup.hide();
+        this._ebox.x = 0;
+
+        Main.chrome.removeInputRegionActor(this._hbox);
+    },
+
+    destroy: function() {
+        if (this._widget.destroy)
+            this._widget.destroy();
+    }
+};
+
diff --git a/src/shell-global.c b/src/shell-global.c
index 42a79b3..cd44571 100644
--- a/src/shell-global.c
+++ b/src/shell-global.c
@@ -668,6 +668,19 @@ shell_get_button_event_click_count(ClutterEvent *event)
 }
 
 /**
+ * shell_get_event_related:
+ *
+ * Return value: (transfer none): related actor
+ */
+ClutterActor *
+shell_get_event_related (ClutterEvent *event)
+{
+  g_return_val_if_fail (event->type == CLUTTER_ENTER ||
+                        event->type == CLUTTER_LEAVE, NULL);
+  return event->crossing.related;
+}
+
+/**
  * shell_global_get:
  *
  * Gets the singleton global object that represents the desktop.
@@ -1216,3 +1229,50 @@ shell_global_create_root_pixmap_actor (ShellGlobal *global)
 
   return clone;
 }
+
+void
+shell_global_clutter_cairo_texture_draw_clock (ClutterCairoTexture *texture,
+                                               int                  hour,
+                                               int                  minute)
+{
+  cairo_t *cr;
+  guint width, height;
+  double xc, yc, radius, hour_radius, minute_radius;
+  double angle;
+
+  clutter_cairo_texture_get_surface_size (texture, &width, &height);
+  xc = (double)width / 2;
+  yc = (double)height / 2;
+  radius = (double)(MIN(width, height)) / 2 - 2;
+  minute_radius = radius - 3;
+  hour_radius = radius / 2;
+
+  clutter_cairo_texture_clear (texture);
+  cr = clutter_cairo_texture_create (texture);
+  cairo_set_line_width (cr, 1.0);
+
+  /* Outline */
+  cairo_arc (cr, xc, yc, radius, 0.0, 2.0 * M_PI);
+  cairo_stroke (cr);
+
+  /* Hour hand. (We add a fraction to @hour for the minutes, then
+   * convert to radians, and then subtract pi/2 because cairo's origin
+   * is at 3:00, not 12:00.)
+   */
+  angle = ((hour + minute / 60.0) / 12.0) * 2.0 * M_PI - M_PI / 2.0;
+  cairo_move_to (cr, xc, yc);
+  cairo_line_to (cr,
+                 xc + hour_radius * cos (angle),
+                 yc + hour_radius * sin (angle));
+  cairo_stroke (cr);
+
+  /* Minute hand */
+  angle = (minute / 60.0) * 2.0 * M_PI - M_PI / 2.0;
+  cairo_move_to (cr, xc, yc);
+  cairo_line_to (cr,
+                 xc + minute_radius * cos (angle),
+                 yc + minute_radius * sin (angle));
+  cairo_stroke (cr);
+
+  cairo_destroy (cr);
+}
diff --git a/src/shell-global.h b/src/shell-global.h
index f263890..a23f377 100644
--- a/src/shell-global.h
+++ b/src/shell-global.h
@@ -48,6 +48,8 @@ guint16 shell_get_event_key_symbol(ClutterEvent *event);
 
 guint16 shell_get_button_event_click_count(ClutterEvent *event);
 
+ClutterActor *shell_get_event_related(ClutterEvent *event);
+
 ShellGlobal *shell_global_get (void);
 
 void shell_global_grab_dbus_service (ShellGlobal *global);
@@ -82,6 +84,10 @@ ClutterCairoTexture *shell_global_create_vertical_gradient (ClutterColor *top,
 
 ClutterActor *shell_global_create_root_pixmap_actor (ShellGlobal *global);
 
+void shell_global_clutter_cairo_texture_draw_clock (ClutterCairoTexture *texture,
+						    int                  hour,
+						    int                  minute);
+
 G_END_DECLS
 
 #endif /* __SHELL_GLOBAL_H__ */



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