diff --git a/js/ui/Makefile.am b/js/ui/Makefile.am index 8465b9e84..e344982ae 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 6886f8aae..a9d8f0733 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 000000000..0afd8d8db --- /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 000000000..2e38bf02f --- /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 000000000..78baf86b4 --- /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 42a79b332..cd445714a 100644 --- a/src/shell-global.c +++ b/src/shell-global.c @@ -667,6 +667,19 @@ shell_get_button_event_click_count(ClutterEvent *event) return event->button.click_count; } +/** + * 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: * @@ -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 f263890aa..a23f37735 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__ */