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