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
This commit is contained in:
parent
bc558306a4
commit
10afe46195
@ -14,6 +14,9 @@ dist_jsui_DATA = \
|
|||||||
overlay.js \
|
overlay.js \
|
||||||
panel.js \
|
panel.js \
|
||||||
runDialog.js \
|
runDialog.js \
|
||||||
|
sidebar.js \
|
||||||
tweener.js \
|
tweener.js \
|
||||||
|
widget.js \
|
||||||
|
widgetBox.js \
|
||||||
windowManager.js \
|
windowManager.js \
|
||||||
workspaces.js
|
workspaces.js
|
||||||
|
@ -13,6 +13,7 @@ const Chrome = imports.ui.chrome;
|
|||||||
const Overlay = imports.ui.overlay;
|
const Overlay = imports.ui.overlay;
|
||||||
const Panel = imports.ui.panel;
|
const Panel = imports.ui.panel;
|
||||||
const RunDialog = imports.ui.runDialog;
|
const RunDialog = imports.ui.runDialog;
|
||||||
|
const Sidebar = imports.ui.sidebar;
|
||||||
const Tweener = imports.ui.tweener;
|
const Tweener = imports.ui.tweener;
|
||||||
const WindowManager = imports.ui.windowManager;
|
const WindowManager = imports.ui.windowManager;
|
||||||
|
|
||||||
@ -21,6 +22,7 @@ DEFAULT_BACKGROUND_COLOR.from_pixel(0x2266bbff);
|
|||||||
|
|
||||||
let chrome = null;
|
let chrome = null;
|
||||||
let panel = null;
|
let panel = null;
|
||||||
|
let sidebar = null;
|
||||||
let overlay = null;
|
let overlay = null;
|
||||||
let runDialog = null;
|
let runDialog = null;
|
||||||
let wm = null;
|
let wm = null;
|
||||||
@ -66,6 +68,7 @@ function start() {
|
|||||||
overlay = new Overlay.Overlay();
|
overlay = new Overlay.Overlay();
|
||||||
chrome = new Chrome.Chrome();
|
chrome = new Chrome.Chrome();
|
||||||
panel = new Panel.Panel();
|
panel = new Panel.Panel();
|
||||||
|
sidebar = new Sidebar.Sidebar();
|
||||||
wm = new WindowManager.WindowManager();
|
wm = new WindowManager.WindowManager();
|
||||||
|
|
||||||
global.screen.connect('toggle-recording', function() {
|
global.screen.connect('toggle-recording', function() {
|
||||||
|
153
js/ui/sidebar.js
Normal file
153
js/ui/sidebar.js
Normal file
@ -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));
|
||||||
|
}
|
||||||
|
};
|
293
js/ui/widget.js
Normal file
293
js/ui/widget.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
};
|
359
js/ui/widgetBox.js
Normal file
359
js/ui/widgetBox.js
Normal file
@ -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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -667,6 +667,19 @@ shell_get_button_event_click_count(ClutterEvent *event)
|
|||||||
return event->button.click_count;
|
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:
|
* shell_global_get:
|
||||||
*
|
*
|
||||||
@ -1216,3 +1229,50 @@ shell_global_create_root_pixmap_actor (ShellGlobal *global)
|
|||||||
|
|
||||||
return clone;
|
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);
|
||||||
|
}
|
||||||
|
@ -48,6 +48,8 @@ guint16 shell_get_event_key_symbol(ClutterEvent *event);
|
|||||||
|
|
||||||
guint16 shell_get_button_event_click_count(ClutterEvent *event);
|
guint16 shell_get_button_event_click_count(ClutterEvent *event);
|
||||||
|
|
||||||
|
ClutterActor *shell_get_event_related(ClutterEvent *event);
|
||||||
|
|
||||||
ShellGlobal *shell_global_get (void);
|
ShellGlobal *shell_global_get (void);
|
||||||
|
|
||||||
void shell_global_grab_dbus_service (ShellGlobal *global);
|
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);
|
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
|
G_END_DECLS
|
||||||
|
|
||||||
#endif /* __SHELL_GLOBAL_H__ */
|
#endif /* __SHELL_GLOBAL_H__ */
|
||||||
|
Loading…
Reference in New Issue
Block a user