30d3c1fe72
Using a larger area around the hot corner to decide if the user has left the hot corner prevents triggering the hot corner multiple times due to an accidental jitter.
469 lines
20 KiB
JavaScript
469 lines
20 KiB
JavaScript
/* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
|
|
|
|
const Big = imports.gi.Big;
|
|
const Clutter = imports.gi.Clutter;
|
|
const Gtk = imports.gi.Gtk;
|
|
const Lang = imports.lang;
|
|
const Mainloop = imports.mainloop;
|
|
const Meta = imports.gi.Meta;
|
|
const Shell = imports.gi.Shell;
|
|
const Tweener = imports.ui.tweener;
|
|
const Signals = imports.signals;
|
|
const Gettext = imports.gettext.domain('gnome-shell');
|
|
const _ = Gettext.gettext;
|
|
|
|
const Button = imports.ui.button;
|
|
const Main = imports.ui.main;
|
|
|
|
const PANEL_HEIGHT = 26;
|
|
const TRAY_HEIGHT = PANEL_HEIGHT - 1;
|
|
|
|
const DEFAULT_PADDING = 4;
|
|
|
|
const PANEL_ICON_SIZE = 24;
|
|
|
|
const BACKGROUND_TOP = new Clutter.Color();
|
|
BACKGROUND_TOP.from_pixel(0x414141ff);
|
|
const BACKGROUND_BOTTOM = new Clutter.Color();
|
|
BACKGROUND_BOTTOM.from_pixel(0x000000ff);
|
|
|
|
const PANEL_FOREGROUND_COLOR = new Clutter.Color();
|
|
PANEL_FOREGROUND_COLOR.from_pixel(0xffffffff);
|
|
const SN_BACKGROUND_COLOR = new Clutter.Color();
|
|
SN_BACKGROUND_COLOR.from_pixel(0xffff00a0);
|
|
|
|
const TRANSPARENT_COLOR = new Clutter.Color();
|
|
TRANSPARENT_COLOR.from_pixel(0x00000000);
|
|
|
|
// Don't make the mouse hover effect visible to the user for a menu feel.
|
|
const PANEL_BUTTON_COLOR = new Clutter.Color();
|
|
PANEL_BUTTON_COLOR.from_pixel(0x00000000);
|
|
|
|
// Lighten pressed buttons; darkening has no effect on a black background.
|
|
const PRESSED_BUTTON_BACKGROUND_COLOR = new Clutter.Color();
|
|
PRESSED_BUTTON_BACKGROUND_COLOR.from_pixel(0x324c6ffa);
|
|
|
|
const DEFAULT_FONT = 'Sans 16px';
|
|
|
|
const TRAY_PADDING = 0;
|
|
// See comments around _recomputeTraySize
|
|
const TRAY_SPACING = 14;
|
|
const TRAY_SPACING_MIN = 8;
|
|
|
|
// Used for the tray icon container with gtk pre-2.16, which doesn't
|
|
// fully support tray icon transparency
|
|
const TRAY_BACKGROUND_COLOR = new Clutter.Color();
|
|
TRAY_BACKGROUND_COLOR.from_pixel(0xefefefff);
|
|
const TRAY_BORDER_COLOR = new Clutter.Color();
|
|
TRAY_BORDER_COLOR.from_pixel(0x00000033);
|
|
const TRAY_CORNER_RADIUS = 5;
|
|
const TRAY_BORDER_WIDTH = 0;
|
|
|
|
|
|
function AppPanelMenu() {
|
|
this._init();
|
|
}
|
|
|
|
AppPanelMenu.prototype = {
|
|
_init: function() {
|
|
this._metaDisplay = Shell.Global.get().screen.get_display();
|
|
|
|
this._focusedApp = null;
|
|
this._activeSequence = null;
|
|
this._startupSequences = {};
|
|
|
|
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
spacing: DEFAULT_PADDING,
|
|
y_align: Big.BoxAlignment.CENTER });
|
|
this._iconBox = new Big.Box({ width: PANEL_ICON_SIZE, height: PANEL_ICON_SIZE,
|
|
x_align: Big.BoxAlignment.CENTER,
|
|
y_align: Big.BoxAlignment.CENTER });
|
|
this.actor.append(this._iconBox, Big.BoxPackFlags.NONE);
|
|
let labelBox = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
|
|
y_align: Big.BoxAlignment.CENTER });
|
|
this._label = new Clutter.Text({ font_name: DEFAULT_FONT,
|
|
color: PANEL_FOREGROUND_COLOR,
|
|
text: "" });
|
|
labelBox.append(this._label, Big.BoxPackFlags.EXPAND);
|
|
this.actor.append(labelBox, Big.BoxPackFlags.NONE);
|
|
|
|
this._startupBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
y_align: Big.BoxAlignment.CENTER });
|
|
this.actor.append(this._startupBox, Big.BoxPackFlags.NONE);
|
|
|
|
Main.overview.connect('hiding', Lang.bind(this, function () {
|
|
this.actor.opacity = 255;
|
|
}));
|
|
Main.overview.connect('showing', Lang.bind(this, function () {
|
|
this.actor.opacity = 192;
|
|
}));
|
|
|
|
this._metaDisplay.connect('notify::focus-window', Lang.bind(this, function () {
|
|
this._sync();
|
|
}));
|
|
Shell.AppMonitor.get_default().connect('startup-sequence-changed', Lang.bind(this, function() {
|
|
this._sync();
|
|
}));
|
|
this._sync();
|
|
},
|
|
|
|
_sync: function() {
|
|
let appMonitor = Shell.AppMonitor.get_default();
|
|
|
|
let focusWindow = this._metaDisplay.get_focus_window();
|
|
let focusedApp;
|
|
if (focusWindow == null) {
|
|
focusedApp = null;
|
|
} else {
|
|
focusedApp = appMonitor.get_window_app(focusWindow);
|
|
}
|
|
|
|
let lastSequence = null;
|
|
if (focusedApp == null) {
|
|
let sequences = appMonitor.get_startup_sequences();
|
|
if (sequences.length > 0)
|
|
lastSequence = sequences[sequences.length - 1];
|
|
}
|
|
|
|
// If the currently focused app hasn't changed and the current
|
|
// startup sequence hasn't changed, we have nothing to do
|
|
if (focusedApp == this._focusedApp
|
|
&& ((lastSequence == null && this._activeSequence == null)
|
|
|| (lastSequence != null && this._activeSequence != null
|
|
&& lastSequence.get_id() == this._activeSequence.get_id())))
|
|
return;
|
|
|
|
this._focusedApp = focusedApp;
|
|
this._activeSequence = lastSequence;
|
|
|
|
this._iconBox.remove_all();
|
|
this._iconBox.hide();
|
|
this._label.text = '';
|
|
if (this._focusedApp != null) {
|
|
let icon = focusedApp.create_icon_texture(PANEL_ICON_SIZE);
|
|
this._iconBox.append(icon, Big.BoxPackFlags.NONE);
|
|
this._iconBox.show();
|
|
this._label.text = focusedApp.get_name();
|
|
} else if (this._activeSequence != null) {
|
|
let icon = this._activeSequence.create_icon(PANEL_ICON_SIZE);
|
|
this._iconBox.append(icon, Big.BoxPackFlags.NONE);
|
|
this._iconBox.show();
|
|
this._label.text = this._activeSequence.get_name();
|
|
}
|
|
|
|
this.emit('changed');
|
|
}
|
|
}
|
|
|
|
Signals.addSignalMethods(AppPanelMenu.prototype);
|
|
|
|
function Panel() {
|
|
this._init();
|
|
}
|
|
|
|
Panel.prototype = {
|
|
_init : function() {
|
|
let global = Shell.Global.get();
|
|
|
|
|
|
this.actor = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL
|
|
});
|
|
let backgroundGradient = Shell.create_vertical_gradient(BACKGROUND_TOP,
|
|
BACKGROUND_BOTTOM);
|
|
this.actor.connect('notify::allocation', Lang.bind(this, function () {
|
|
let [width, height] = this.actor.get_size();
|
|
backgroundGradient.set_size(width, height);
|
|
}));
|
|
this.actor.add_actor(backgroundGradient);
|
|
|
|
this._leftBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
y_align: Big.BoxAlignment.CENTER,
|
|
spacing: DEFAULT_PADDING,
|
|
padding_right: DEFAULT_PADDING });
|
|
this._centerBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
y_align: Big.BoxAlignment.CENTER });
|
|
this._rightBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
y_align: Big.BoxAlignment.CENTER,
|
|
padding_left: DEFAULT_PADDING });
|
|
|
|
/* This box container ensures that the centerBox is positioned in the *absolute*
|
|
* center, but can be pushed aside if necessary. */
|
|
this._boxContainer = new Shell.GenericContainer();
|
|
this.actor.append(this._boxContainer, Big.BoxPackFlags.EXPAND);
|
|
this._boxContainer.add_actor(this._leftBox);
|
|
this._boxContainer.add_actor(this._centerBox);
|
|
this._boxContainer.add_actor(this._rightBox);
|
|
this._boxContainer.connect('get-preferred-width', Lang.bind(this, function(box, forHeight, alloc) {
|
|
let children = box.get_children();
|
|
for (let i = 0; i < children.length; i++) {
|
|
let [childMin, childNatural] = children[i].get_preferred_width(forHeight);
|
|
alloc.min_size += childMin;
|
|
alloc.natural_size += childNatural;
|
|
}
|
|
}));
|
|
this._boxContainer.connect('get-preferred-height', Lang.bind(this, function(box, forWidth, alloc) {
|
|
let children = box.get_children();
|
|
for (let i = 0; i < children.length; i++) {
|
|
let [childMin, childNatural] = children[i].get_preferred_height(forWidth);
|
|
if (childMin > alloc.min_size)
|
|
alloc.min_size = childMin;
|
|
if (childNatural > alloc.natural_size)
|
|
alloc.natural_size = childNatural;
|
|
}
|
|
}));
|
|
this._boxContainer.connect('allocate', Lang.bind(this, function(container, box, flags) {
|
|
let allocWidth = box.x2 - box.x1;
|
|
let allocHeight = box.y2 - box.y1;
|
|
let [leftMinWidth, leftNaturalWidth] = this._leftBox.get_preferred_width(-1);
|
|
let [centerMinWidth, centerNaturalWidth] = this._centerBox.get_preferred_width(-1);
|
|
let [rightMinWidth, rightNaturalWidth] = this._rightBox.get_preferred_width(-1);
|
|
let leftWidth, centerWidth, rightWidth;
|
|
if (allocWidth < (leftNaturalWidth + centerNaturalWidth + rightNaturalWidth)) {
|
|
leftWidth = leftMinWidth;
|
|
centerWidth = centerMinWidth;
|
|
rightWidth = rightMinWidth;
|
|
} else {
|
|
leftWidth = leftNaturalWidth;
|
|
centerWidth = centerNaturalWidth;
|
|
rightWidth = rightNaturalWidth;
|
|
}
|
|
|
|
let x;
|
|
let childBox = new Clutter.ActorBox();
|
|
childBox.x1 = box.x1;
|
|
childBox.y1 = box.y1;
|
|
childBox.x2 = x = childBox.x1 + leftWidth;
|
|
childBox.y2 = box.y2;
|
|
this._leftBox.allocate(childBox, flags);
|
|
|
|
let centerNaturalX = Math.floor((box.x2 - box.x1) / 2 - (centerWidth / 2));
|
|
/* Check left side */
|
|
if (x < centerNaturalX) {
|
|
/* We didn't overflow the left, use the natural. */
|
|
x = centerNaturalX;
|
|
}
|
|
/* Check right side */
|
|
if (x + centerWidth > (box.x2 - rightWidth)) {
|
|
x = box.x2 - rightWidth - centerWidth;
|
|
}
|
|
childBox = new Clutter.ActorBox();
|
|
childBox.x1 = x;
|
|
childBox.y1 = box.y1;
|
|
childBox.x2 = x = childBox.x1 + centerWidth;
|
|
childBox.y2 = box.y2;
|
|
this._centerBox.allocate(childBox, flags);
|
|
|
|
childBox = new Clutter.ActorBox();
|
|
childBox.x1 = box.x2 - rightWidth;
|
|
childBox.y1 = box.y1;
|
|
childBox.x2 = box.x2;
|
|
childBox.y2 = box.y2;
|
|
this._rightBox.allocate(childBox, flags);
|
|
}));
|
|
|
|
/* left side */
|
|
|
|
this.button = new Button.Button(_("Activities"), PANEL_BUTTON_COLOR, PRESSED_BUTTON_BACKGROUND_COLOR,
|
|
PANEL_FOREGROUND_COLOR, true, DEFAULT_FONT);
|
|
this.button.button.height = PANEL_HEIGHT;
|
|
|
|
this._leftBox.append(this.button.button, Big.BoxPackFlags.NONE);
|
|
|
|
// We use this flag to mark the case where the user has entered the
|
|
// hot corner and has not left both the hot corner and a surrounding
|
|
// guard area (the "environs"). This avoids triggering the hot corner
|
|
// multiple times due to an accidental jitter.
|
|
this._hotCornerEntered = false;
|
|
|
|
this._hotCornerEnvirons = new Clutter.Rectangle({ width: 3,
|
|
height: 3,
|
|
opacity: 0,
|
|
reactive: true });
|
|
|
|
let hotCorner = new Clutter.Rectangle({ width: 1,
|
|
height: 1,
|
|
opacity: 0,
|
|
reactive: true });
|
|
|
|
this._hotCornerEnvirons.connect('leave-event',
|
|
Lang.bind(this, this._onHotCornerEnvironsLeft));
|
|
// Clicking on the hot corner environs should result in the same bahavior
|
|
// as clicking on the hot corner.
|
|
this._hotCornerEnvirons.connect('button-release-event',
|
|
Lang.bind(this, this._onHotCornerClicked));
|
|
|
|
// In addition to being triggered by the mouse enter event, the hot corner
|
|
// can be triggered by clicking on it. This is useful if the user wants to
|
|
// undo the effect of triggering the hot corner once in the hot corner.
|
|
hotCorner.connect('enter-event',
|
|
Lang.bind(this, this._onHotCornerEntered));
|
|
hotCorner.connect('button-release-event',
|
|
Lang.bind(this, this._onHotCornerClicked));
|
|
hotCorner.connect('leave-event',
|
|
Lang.bind(this, this._onHotCornerLeft));
|
|
|
|
this._leftBox.append(this._hotCornerEnvirons, Big.BoxPackFlags.FIXED);
|
|
this._leftBox.append(hotCorner, Big.BoxPackFlags.FIXED);
|
|
|
|
let appMenu = new AppPanelMenu();
|
|
this._leftBox.append(appMenu.actor, Big.BoxPackFlags.NONE);
|
|
|
|
/* center */
|
|
|
|
this._clock = new Clutter.Text({ font_name: DEFAULT_FONT,
|
|
color: PANEL_FOREGROUND_COLOR,
|
|
text: "" });
|
|
this._centerBox.append(this._clock, Big.BoxPackFlags.NONE);
|
|
|
|
/* right */
|
|
|
|
// The tray icons live in trayBox within trayContainer.
|
|
// The trayBox is hidden when there are no tray icons.
|
|
let trayContainer = new Big.Box({ orientation: Big.BoxOrientation.VERTICAL,
|
|
y_align: Big.BoxAlignment.START });
|
|
this._rightBox.append(trayContainer, Big.BoxPackFlags.NONE);
|
|
let trayBox = new Big.Box({ orientation: Big.BoxOrientation.HORIZONTAL,
|
|
height: TRAY_HEIGHT,
|
|
padding: TRAY_PADDING,
|
|
spacing: TRAY_SPACING });
|
|
this._trayBox = trayBox;
|
|
|
|
// gtk+ < 2.16 doesn't have fully-working icon transparency,
|
|
// so we want trayBox to be opaque in that case (the icons
|
|
// will at least pick up its background color).
|
|
if (Gtk.MAJOR_VERSION == 2 && Gtk.MINOR_VERSION < 16) {
|
|
trayBox.background_color = TRAY_BACKGROUND_COLOR;
|
|
trayBox.corner_radius = TRAY_CORNER_RADIUS;
|
|
trayBox.border = TRAY_BORDER_WIDTH;
|
|
trayBox.border_color = TRAY_BORDER_COLOR;
|
|
}
|
|
|
|
trayBox.hide();
|
|
trayContainer.append(trayBox, Big.BoxPackFlags.NONE);
|
|
|
|
this._traymanager = new Shell.TrayManager({ bg_color: TRAY_BACKGROUND_COLOR });
|
|
this._traymanager.connect('tray-icon-added',
|
|
Lang.bind(this, function(o, icon) {
|
|
trayBox.append(icon, Big.BoxPackFlags.NONE);
|
|
|
|
// Make sure the trayBox is shown.
|
|
trayBox.show();
|
|
this._recomputeTraySize();
|
|
}));
|
|
this._traymanager.connect('tray-icon-removed',
|
|
Lang.bind(this, function(o, icon) {
|
|
trayBox.remove_actor(icon);
|
|
|
|
if (trayBox.get_children().length == 0)
|
|
trayBox.hide();
|
|
this._recomputeTraySize();
|
|
}));
|
|
this._traymanager.manage_stage(global.stage);
|
|
|
|
let statusbox = new Big.Box();
|
|
let statusmenu = this._statusmenu = new Shell.StatusMenu();
|
|
statusmenu.get_icon().hide();
|
|
statusmenu.get_name().fontName = DEFAULT_FONT;
|
|
statusmenu.get_name().color = PANEL_FOREGROUND_COLOR;
|
|
statusbox.append(this._statusmenu, Big.BoxPackFlags.NONE);
|
|
let statusbutton = new Button.Button(statusbox,
|
|
PANEL_BUTTON_COLOR,
|
|
PRESSED_BUTTON_BACKGROUND_COLOR,
|
|
PANEL_FOREGROUND_COLOR,
|
|
true);
|
|
statusbutton.button.height = PANEL_HEIGHT;
|
|
statusbutton.button.connect('button-press-event', function (b, e) {
|
|
statusmenu.toggle(e);
|
|
return false;
|
|
});
|
|
this._rightBox.append(statusbutton.button, Big.BoxPackFlags.NONE);
|
|
// We get a deactivated event when the popup disappears
|
|
this._statusmenu.connect('deactivated', function (sm) {
|
|
statusbutton.release();
|
|
});
|
|
|
|
// TODO: decide what to do with the rest of the panel in the Overview mode (make it fade-out, become non-reactive, etc.)
|
|
// We get into the Overview mode on button-press-event as opposed to button-release-event because eventually we'll probably
|
|
// have the Overview act like a menu that allows the user to release the mouse on the activity the user wants
|
|
// to switch to.
|
|
this.button.button.connect('button-press-event',
|
|
Lang.bind(Main.overview, Main.overview.toggle));
|
|
// In addition to pressing the button, the Overview can be entered and exited by other means, such as
|
|
// pressing the System key, Alt+F1 or Esc. We want the button to be pressed in when the Overview is entered
|
|
// and to be released when it is exited regardless of how it was triggered.
|
|
Main.overview.connect('showing', Lang.bind(this.button, this.button.pressIn));
|
|
Main.overview.connect('hiding', Lang.bind(this.button, this.button.release));
|
|
|
|
Main.chrome.addActor(this.actor);
|
|
Main.chrome.setVisibleInOverview(this.actor, true);
|
|
|
|
// Start the clock
|
|
this._updateClock();
|
|
},
|
|
|
|
startupAnimation: function() {
|
|
this.actor.y = -this.actor.height;
|
|
Tweener.addTween(this.actor,
|
|
{ y: 0,
|
|
time: 0.2,
|
|
transition: "easeOutQuad"
|
|
});
|
|
},
|
|
|
|
// By default, tray icons have a spacing of TRAY_SPACING. However this
|
|
// starts to fail if we have too many as can sadly happen; just jump down
|
|
// to a spacing of 8 if we're over 6.
|
|
// http://bugzilla.gnome.org/show_bug.cgi?id=590495
|
|
_recomputeTraySize: function () {
|
|
if (this._trayBox.get_children().length > 6)
|
|
this._trayBox.spacing = TRAY_SPACING_MIN;
|
|
else
|
|
this._trayBox.spacing = TRAY_SPACING;
|
|
},
|
|
|
|
_updateClock: function() {
|
|
let displayDate = new Date();
|
|
let msecRemaining = 60000 - (1000 * displayDate.getSeconds() +
|
|
displayDate.getMilliseconds());
|
|
if (msecRemaining < 500) {
|
|
displayDate.setMinutes(displayDate.getMinutes() + 1);
|
|
msecRemaining += 60000;
|
|
}
|
|
/* Translators: This is a time format. */
|
|
this._clock.set_text(displayDate.toLocaleFormat(_("%a %l:%M %p")));
|
|
Mainloop.timeout_add(msecRemaining, Lang.bind(this, this._updateClock));
|
|
return false;
|
|
},
|
|
|
|
_onHotCornerEntered : function() {
|
|
if (!this._hotCornerEntered) {
|
|
this._hotCornerEntered = true;
|
|
if (!Main.overview.animationInProgress) {
|
|
Main.overview.toggle();
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_onHotCornerClicked : function() {
|
|
if (!Main.overview.animationInProgress) {
|
|
Main.overview.toggle();
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_onHotCornerLeft : function(actor, event) {
|
|
if (Shell.get_event_related(event) != this._hotCornerEnvirons) {
|
|
this._hotCornerEntered = false;
|
|
}
|
|
return false;
|
|
},
|
|
|
|
_onHotCornerEnvironsLeft : function(actor, event) {
|
|
if (Shell.get_event_related(event) != this._hotCorner) {
|
|
this._hotCornerEntered = false;
|
|
}
|
|
return false;
|
|
}
|
|
};
|