[gnome-shell] Add an initial implementation of the sidebar
- From: Dan Winship <danw src gnome org>
- To: svn-commits-list gnome org
- Subject: [gnome-shell] Add an initial implementation of the sidebar
- Date: Fri, 15 May 2009 17:04:46 -0400 (EDT)
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]