gnome-shell/js/ui/screenShield.js
Giovanni Campagna 2c627cad10 ScreenShield: prepare the dialog when the user starts dragging
This way if the user drags long enough it will see the dialog
below the curtain, and it will appear as it was always there.

https://bugzilla.gnome.org/show_bug.cgi?id=681143
2012-08-13 17:59:19 +02:00

623 lines
22 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
const Clutter = imports.gi.Clutter;
const Gio = imports.gi.Gio;
const GnomeDesktop = imports.gi.GnomeDesktop;
const Lang = imports.lang;
const Meta = imports.gi.Meta;
const Signals = imports.signals;
const St = imports.gi.St;
const GnomeSession = imports.misc.gnomeSession;
const Lightbox = imports.ui.lightbox;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const Tweener = imports.ui.tweener;
const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
const LOCK_ENABLED_KEY = 'lock-enabled';
const CURTAIN_SLIDE_TIME = 0.8;
// fraction of screen height the arrow must reach before completing
// the slide up automatically
const ARROW_DRAG_TRESHOLD = 0.1;
const SUMMARY_ICON_SIZE = 48;
// Lightbox fading times
// STANDARD_FADE_TIME is used when the session goes idle, while
// SHORT_FADE_TIME is used when requesting lock explicitly from the user menu
const STANDARD_FADE_TIME = 10;
const SHORT_FADE_TIME = 0.8;
const Clock = new Lang.Class({
Name: 'ScreenShieldClock',
CLOCK_FORMAT_KEY: 'clock-format',
CLOCK_SHOW_SECONDS_KEY: 'clock-show-seconds',
_init: function() {
this.actor = new St.BoxLayout({ style_class: 'screen-shield-clock',
vertical: true });
this._time = new St.Label({ style_class: 'screen-shield-clock-time' });
this._date = new St.Label({ style_class: 'screen-shield-clock-date' });
this.actor.add(this._time, { x_align: St.Align.MIDDLE });
this.actor.add(this._date, { x_align: St.Align.MIDDLE });
this._wallClock = new GnomeDesktop.WallClock({ time_only: true });
this._wallClock.connect('notify::clock', Lang.bind(this, this._updateClock));
this._updateClock();
},
_updateClock: function() {
this._time.text = this._wallClock.clock;
let date = new Date();
/* Translators: This is a time format for a date in
long format */
this._date.text = date.toLocaleFormat(_("%A, %B %d"));
},
destroy: function() {
this.actor.destroy();
this._wallClock.run_dispose();
}
});
const NotificationsBox = new Lang.Class({
Name: 'NotificationsBox',
_init: function() {
this.actor = new St.BoxLayout({ vertical: true,
name: 'screenShieldNotifications',
style_class: 'screen-shield-notifications-box' });
this._residentNotificationBox = new St.BoxLayout({ vertical: true,
style_class: 'screen-shield-notifications-box' });
let scrollView = new St.ScrollView({ x_fill: false, x_align: St.Align.MIDDLE });
this._persistentNotificationBox = new St.BoxLayout({ vertical: true,
style_class: 'screen-shield-notifications-box' });
scrollView.add_actor(this._persistentNotificationBox);
this.actor.add(this._residentNotificationBox, { x_fill: true });
this.actor.add(scrollView, { x_fill: true, x_align: St.Align.MIDDLE });
this._items = [];
Main.messageTray.getSummaryItems().forEach(Lang.bind(this, function(item) {
this._summaryItemAdded(Main.messageTray, item);
}));
this._summaryAddedId = Main.messageTray.connect('summary-item-added', Lang.bind(this, this._summaryItemAdded));
},
destroy: function() {
if (this._summaryAddedId) {
Main.messageTray.disconnect(this._summaryAddedId);
this._summaryAddedId = 0;
}
for (let i = 0; i < this._items.length; i++)
this._removeItem(this._items[i]);
this._items = [];
this.actor.destroy();
},
_updateVisibility: function() {
if (this._residentNotificationBox.get_n_children() > 0) {
this.actor.show();
return;
}
let children = this._persistentNotificationBox.get_children()
this.actor.visible = children.some(function(a) { return a.visible; });
},
_sourceIsResident: function(source) {
return source.hasResidentNotification() && !source.isChat;
},
_makeNotificationCountText: function(count, isChat) {
if (isChat)
return ngettext("%d new message", "%d new messages", count).format(count);
else
return ngettext("%d new notification", "%d new notifications", count).format(count);
},
_makeNotificationSource: function(source) {
let box = new St.BoxLayout({ style_class: 'screen-shield-notification-source' });
let iconClone = source.createIcon(SUMMARY_ICON_SIZE);
let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE);
sourceActor.setIcon(iconClone);
box.add(sourceActor.actor, { y_fill: true });
let textBox = new St.BoxLayout({ vertical: true });
box.add(textBox);
let label = new St.Label({ text: source.title,
style_class: 'screen-shield-notification-label' });
textBox.add(label);
let count = source.unseenCount;
let countLabel = new St.Label({ text: this._makeNotificationCountText(count, source.isChat),
style_class: 'screen-shield-notification-count-text' });
textBox.add(countLabel);
box.visible = count != 0;
return [box, countLabel];
},
_summaryItemAdded: function(tray, item) {
// Ignore transient sources
if (item.source.isTransient)
return;
let obj = {
item: item,
source: item.source,
resident: this._sourceIsResident(item.source),
contentUpdatedId: 0,
sourceDestroyId: 0,
sourceBox: null,
countLabel: null,
};
if (obj.resident) {
item.prepareNotificationStackForShowing();
this._residentNotificationBox.add(item.notificationStackView);
} else {
[obj.sourceBox, obj.countLabel] = this._makeNotificationSource(item.source);
this._persistentNotificationBox.add(obj.sourceBox, { x_fill: false, x_align: St.Align.MIDDLE });
}
obj.contentUpdatedId = item.connect('content-updated', Lang.bind(this, this._onItemContentUpdated));
obj.sourceCountChangedId = item.source.connect('count-updated', Lang.bind(this, this._onSourceChanged));
obj.sourceTitleChangedId = item.source.connect('title-changed', Lang.bind(this, this._onSourceChanged));
obj.sourceDestroyId = item.source.connect('destroy', Lang.bind(this, this._onSourceDestroy));
this._items.push(obj);
this._updateVisibility();
},
_findSource: function(source) {
for (let i = 0; i < this._items.length; i++) {
if (this._items[i].source == source)
return i;
}
return -1;
},
_onItemContentUpdated: function(item) {
let obj = this._items[this._findSource(item.source)];
this._updateItem(obj);
},
_onSourceChanged: function(source) {
let obj = this._items[this._findSource(source)];
this._updateItem(obj);
},
_updateItem: function(obj) {
let itemShouldBeResident = this._sourceIsResident(obj.source);
if (itemShouldBeResident && obj.resident) {
// Nothing to do here, the actor is already updated
return;
}
if (obj.resident && !itemShouldBeResident) {
// make into a regular item
this._residentNotificationBox.remove_actor(obj.item.notificationStackView);
[obj.sourceBox, obj.countLabel] = this._makeNotificationSource(obj.source);
this._persistentNotificationBox.add(obj.sourceBox);
} else if (itemShouldBeResident && !obj.resident) {
// make into a resident item
obj.sourceBox.destroy();
obj.sourceBox = obj.countLabel = null;
obj.resident = true;
obj.item.prepareNotificationStackForShowing();
this._residentNotificationBox.add(obj.item.notificationStackView);
} else {
// just update the counter
let count = obj.source.unseenCount;
obj.countLabel.text = this._makeNotificationCountText(count, obj.source.isChat);
obj.sourceBox.visible = count != 0;
}
this._updateVisibility();
},
_onSourceDestroy: function(source) {
let idx = this._findSource(source);
this._removeItem(this._items[idx]);
this._items.splice(idx, 1);
this._updateVisibility();
},
_removeItem: function(obj) {
if (obj.resident) {
this._residentNotificationBox.remove_actor(obj.item.notificationStackView);
obj.item.doneShowingNotificationStack();
} else {
obj.sourceBox.destroy();
}
obj.item.disconnect(obj.contentUpdatedId);
obj.source.disconnect(obj.sourceDestroyId);
obj.source.disconnect(obj.sourceCountChangedId);
},
});
const LockDialogGroup = new Lang.Class({
Name: 'LockDialogGroup',
Extends: St.Widget,
_init: function(params) {
this.parent(params);
},
vfunc_get_preferred_height: function(forWidth) {
return [global.screen_height, global.screen_height];
},
vfunc_get_preferred_width: function(forHeight) {
return [global.screen_width, global.screen_width];
}
});
/**
* To test screen shield, make sure to kill gnome-screensaver.
*
* If you are setting org.gnome.desktop.session.idle-delay directly in dconf,
* rather than through System Settings, you also need to set
* org.gnome.settings-daemon.plugins.power.sleep-display-ac and
* org.gnome.settings-daemon.plugins.power.sleep-display-battery to the same value.
* This will ensure that the screen blanks at the right time when it fades out.
* https://bugzilla.gnome.org/show_bug.cgi?id=668703 explains the dependance.
*/
const ScreenShield = new Lang.Class({
Name: 'ScreenShield',
_init: function() {
this.actor = Main.layoutManager.screenShieldGroup;
this._lockScreenGroup = new St.Widget({ x_expand: true,
y_expand: true,
reactive: true,
can_focus: true,
layout_manager: new Clutter.BinLayout(),
name: 'lockScreenGroup',
});
this._lockScreenGroup.connect('key-release-event',
Lang.bind(this, this._onLockScreenKeyRelease));
this._background = Meta.BackgroundActor.new_for_screen(global.screen);
this._lockScreenGroup.add_actor(this._background);
// FIXME: build the rest of the lock screen here
this._arrow = new St.DrawingArea({ style_class: 'arrow',
reactive: true,
x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.END,
// HACK: without these, ClutterBinLayout
// ignores alignment properties on the actor
x_expand: true,
y_expand: true
});
this._arrow.connect('repaint', Lang.bind(this, this._drawArrow));
this._lockScreenGroup.add_actor(this._arrow);
let action = new Clutter.DragAction({ drag_axis: Clutter.DragAxis.Y_AXIS });
action.connect('drag-begin', Lang.bind(this, this._onDragBegin));
action.connect('drag-end', Lang.bind(this, this._onDragEnd));
this._lockScreenGroup.add_action(action);
this._lockDialogGroup = new LockDialogGroup({ name: 'lockDialogGroup' });
this.actor.add_actor(this._lockDialogGroup);
this.actor.add_actor(this._lockScreenGroup);
this._presence = new GnomeSession.Presence(Lang.bind(this, function(proxy, error) {
if (error) {
logError(error, 'Error while reading gnome-session presence');
return;
}
this._onStatusChanged(proxy.status);
}));
this._presence.connectSignal('StatusChanged', Lang.bind(this, function(proxy, senderName, [status]) {
this._onStatusChanged(status);
}));
this._settings = new Gio.Settings({ schema: SCREENSAVER_SCHEMA });
this._isModal = false;
this._isLocked = false;
this._hasLockScreen = false;
this._lightbox = new Lightbox.Lightbox(Main.uiGroup,
{ inhibitEvents: true,
fadeInTime: STANDARD_FADE_TIME,
fadeFactor: 1 });
},
_onLockScreenKeyRelease: function(actor, event) {
if (event.get_key_symbol() == Clutter.KEY_Escape) {
this._ensureUnlockDialog();
this._hideLockScreen(true);
return true;
}
return false;
},
_drawArrow: function() {
let cr = this._arrow.get_context();
let [w, h] = this._arrow.get_surface_size();
let node = this._arrow.get_theme_node();
Clutter.cairo_set_source_color(cr, node.get_foreground_color());
cr.moveTo(0, h);
cr.lineTo(w/2, 0);
cr.lineTo(w, h);
cr.fill();
},
_onDragBegin: function() {
Tweener.removeTweens(this._lockScreenGroup);
this._ensureUnlockDialog();
},
_onDragEnd: function(action, actor, eventX, eventY, modifiers) {
if (this._lockScreenGroup.y < -(ARROW_DRAG_TRESHOLD * global.stage.height)) {
// Complete motion automatically
this._hideLockScreen(true);
} else {
// restore the lock screen to its original place
// try to use the same speed as the normal animation
let h = global.stage.height;
let time = CURTAIN_SLIDE_TIME * (-this._lockScreenGroup.y) / h;
Tweener.removeTweens(this._lockScreenGroup);
Tweener.addTween(this._lockScreenGroup,
{ y: 0,
time: time,
transition: 'linear',
onComplete: function() {
this.fixed_position_set = false;
}
});
// If we have a unlock dialog, cancel it
if (this._dialog) {
this._dialog.cancel();
if (!this._keepDialog) {
this._dialog = null;
}
}
}
},
_onStatusChanged: function(status) {
if (status == GnomeSession.PresenceStatus.IDLE) {
if (this._dialog) {
this._dialog.cancel();
if (!this._keepDialog) {
this._dialog = null;
}
}
if (!this._isModal) {
Main.pushModal(this.actor);
this._isModal = true;
}
if (!this._isLocked)
this._lightbox.show();
} else {
let lightboxWasShown = this._lightbox.shown;
this._lightbox.hide();
let shouldLock = lightboxWasShown && this._settings.get_boolean(LOCK_ENABLED_KEY);
if (shouldLock || this._isLocked) {
this.lock(false);
} else if (this._isModal) {
this.unlock();
}
}
},
showDialog: function() {
this.lock(true);
this._ensureUnlockDialog();
this._hideLockScreen(false);
},
_hideLockScreen: function(animate) {
if (animate) {
// Tween the lock screen out of screen
// try to use the same speed regardless of original position
let h = global.stage.height;
let time = CURTAIN_SLIDE_TIME * (h + this._lockScreenGroup.y) / h;
Tweener.removeTweens(this._lockScreenGroup);
Tweener.addTween(this._lockScreenGroup,
{ y: -h,
time: time,
transition: 'linear',
onComplete: function() { this.hide(); }
});
} else {
this._lockScreenGroup.hide();
}
},
_ensureUnlockDialog: function() {
if (!this._dialog) {
[this._dialog, this._keepDialog] = Main.sessionMode.createUnlockDialog(this._lockDialogGroup);
if (!this._dialog) {
// This session mode has no locking capabilities
this.unlock();
return;
}
this._dialog.connect('loaded', Lang.bind(this, function() {
if (!this._dialog.open()) {
log('Could not open login dialog: failed to acquire grab');
this.unlock();
}
}));
this._dialog.connect('failed', Lang.bind(this, this._onUnlockFailed));
this._dialog.connect('unlocked', Lang.bind(this, this._onUnlockSucceded));
}
if (this._keepDialog) {
// Notify the other components that even though we are showing the
// screenshield, we're not in a locked state
// (this happens for the gdm greeter)
this._isLocked = false;
this.emit('lock-status-changed', false);
}
},
_onUnlockFailed: function() {
this._dialog.destroy();
this._dialog = null;
this._resetLockScreen(false);
},
_onUnlockSucceded: function() {
this.unlock();
},
_resetLockScreen: function(animate) {
this._lockScreenGroup.show();
if (animate) {
this._lockScreenGroup.y = -global.screen_height;
Tweener.removeTweens(this._lockScreenGroup);
Tweener.addTween(this._lockScreenGroup,
{ y: 0,
time: SHORT_FADE_TIME,
transition: 'linear',
onComplete: function() {
this._lockScreenGroup.fixed_position_set = false;
this.emit('lock-screen-shown');
},
onCompleteScope: this
});
this._lockDialogGroup.opacity = 0;
Tweener.removeTweens(this._lockDialogGroup);
Tweener.addTween(this._lockDialogGroup,
{ opacity: 255,
time: SHORT_FADE_TIME,
transition: 'easeOutQuad' });
} else {
this._lockScreenGroup.fixed_position_set = false;
this._lockDialogGroup.opacity = 255;
this.emit('lock-screen-shown');
}
this._lockScreenGroup.grab_key_focus();
},
// Some of the actors in the lock screen are heavy in
// resources, so we only create them when needed
_prepareLockScreen: function() {
this._lockScreenContentsBox = new St.BoxLayout({ x_align: Clutter.ActorAlign.CENTER,
y_align: Clutter.ActorAlign.CENTER,
x_expand: true,
y_expand: true,
vertical: true });
this._clock = new Clock();
this._lockScreenContentsBox.add(this._clock.actor, { x_fill: true,
y_fill: true });
this._lockScreenGroup.add_actor(this._lockScreenContentsBox);
if (this._settings.get_boolean('show-notifications')) {
this._notificationsBox = new NotificationsBox();
this._lockScreenContentsBox.add(this._notificationsBox.actor, { x_fill: true,
y_fill: true,
expand: true });
}
this._hasLockScreen = true;
},
_clearLockScreen: function() {
this._clock.destroy();
this._clock = null;
if (this._notificationsBox) {
this._notificationsBox.destroy();
this._notificationsBox = null;
}
this._lockScreenContentsBox.destroy();
this._hasLockScreen = false;
},
get locked() {
return this._isLocked;
},
unlock: function() {
if (this._hasLockScreen)
this._clearLockScreen();
if (this._keepDialog) {
// The dialog must be kept alive,
// so immediately go back to it
// This will also reset _isLocked
this._showUnlockDialog(false);
return;
}
if (this._dialog) {
this._dialog.destroy();
this._dialog = null;
}
this._lightbox.hide();
Main.popModal(this.actor);
this.actor.hide();
this._isModal = false;
this._isLocked = false;
this.emit('lock-status-changed', false);
},
lock: function(animate) {
if (!this._hasLockScreen)
this._prepareLockScreen();
if (!this._isModal) {
Main.pushModal(this.actor);
this._isModal = true;
}
this._isLocked = true;
this.actor.show();
this._resetLockScreen(animate);
this.emit('lock-status-changed', true);
},
});
Signals.addSignalMethods(ScreenShield.prototype);