gnome-shell/js/ui/unlockDialog.js
Florian Müllner ccc64e2621 unlockDialog: Only show count for multiple notifications
In the common case where we only have a single unread notification
from a particular app, the count doesn't add useful information.
Reduce clutter a bit by only showing the notification count if we
have at least two.

https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/997
2020-02-13 20:55:33 +01:00

814 lines
25 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
/* exported UnlockDialog */
const { AccountsService, Atk, Clutter, Gdm,
GnomeDesktop, GLib, GObject, Meta, Shell, St } = imports.gi;
const Background = imports.ui.background;
const Layout = imports.ui.layout;
const Main = imports.ui.main;
const MessageTray = imports.ui.messageTray;
const SwipeTracker = imports.ui.swipeTracker;
const AuthPrompt = imports.gdm.authPrompt;
// The timeout before going back automatically to the lock screen (in seconds)
const IDLE_TIMEOUT = 2 * 60;
// The timeout before showing the unlock hint (in seconds)
const HINT_TIMEOUT = 4;
const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
const CROSSFADE_TIME = 300;
const FADE_OUT_TRANSLATION = 200;
const FADE_OUT_SCALE = 0.3;
const BLUR_BRIGHTNESS = 0.55;
const BLUR_SIGMA = 60;
const SUMMARY_ICON_SIZE = 32;
var NotificationsBox = GObject.registerClass({
Signals: { 'wake-up-screen': {} },
}, class NotificationsBox extends St.BoxLayout {
_init() {
super._init({
vertical: true,
name: 'unlockDialogNotifications',
style_class: 'unlock-dialog-notifications-container',
});
this._scrollView = new St.ScrollView({ hscrollbar_policy: St.PolicyType.NEVER });
this._notificationBox = new St.BoxLayout({
vertical: true,
style_class: 'unlock-dialog-notifications-container',
});
this._scrollView.add_actor(this._notificationBox);
this.add_child(this._scrollView);
this._sources = new Map();
Main.messageTray.getSources().forEach(source => {
this._sourceAdded(Main.messageTray, source, true);
});
this._updateVisibility();
this._sourceAddedId = Main.messageTray.connect('source-added', this._sourceAdded.bind(this));
this.connect('destroy', this._onDestroy.bind(this));
}
_onDestroy() {
if (this._sourceAddedId) {
Main.messageTray.disconnect(this._sourceAddedId);
this._sourceAddedId = 0;
}
let items = this._sources.entries();
for (let [source, obj] of items)
this._removeSource(source, obj);
}
_updateVisibility() {
this._notificationBox.visible =
this._notificationBox.get_children().some(a => a.visible);
this.visible = this._notificationBox.visible;
}
_makeNotificationSource(source, box) {
let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE);
box.add_child(sourceActor);
let textBox = new St.BoxLayout({
x_expand: true,
y_expand: true,
y_align: Clutter.ActorAlign.CENTER,
});
box.add_child(textBox);
let title = new St.Label({
text: source.title,
style_class: 'unlock-dialog-notification-label',
x_expand: true,
x_align: Clutter.ActorAlign.START,
});
textBox.add(title);
let count = source.unseenCount;
let countLabel = new St.Label({
text: `${count}`,
visible: count > 1,
style_class: 'unlock-dialog-notification-count-text',
});
textBox.add(countLabel);
box.visible = count !== 0;
return [title, countLabel];
}
_makeNotificationDetailedSource(source, box) {
let sourceActor = new MessageTray.SourceActor(source, SUMMARY_ICON_SIZE);
let sourceBin = new St.Bin({ child: sourceActor });
box.add(sourceBin);
let textBox = new St.BoxLayout({ vertical: true });
box.add_child(textBox);
let title = new St.Label({
text: source.title,
style_class: 'unlock-dialog-notification-label',
});
textBox.add(title);
let visible = false;
for (let i = 0; i < source.notifications.length; i++) {
let n = source.notifications[i];
if (n.acknowledged)
continue;
let body = '';
if (n.bannerBodyText) {
body = n.bannerBodyMarkup
? n.bannerBodyText
: GLib.markup_escape_text(n.bannerBodyText, -1);
}
let label = new St.Label({ style_class: 'unlock-dialog-notification-count-text' });
label.clutter_text.set_markup(`<b>${n.title}</b> ${body}`);
textBox.add(label);
visible = true;
}
box.visible = visible;
return [title, null];
}
_shouldShowDetails(source) {
return source.policy.detailsInLockScreen ||
source.narrowestPrivacyScope === MessageTray.PrivacyScope.SYSTEM;
}
_updateSourceBoxStyle(source, obj, box) {
let hasCriticalNotification =
source.notifications.some(n => n.urgency === MessageTray.Urgency.CRITICAL);
if (hasCriticalNotification !== obj.hasCriticalNotification) {
obj.hasCriticalNotification = hasCriticalNotification;
if (hasCriticalNotification)
box.add_style_class_name('critical');
else
box.remove_style_class_name('critical');
}
}
_showSource(source, obj, box) {
if (obj.detailed)
[obj.titleLabel, obj.countLabel] = this._makeNotificationDetailedSource(source, box);
else
[obj.titleLabel, obj.countLabel] = this._makeNotificationSource(source, box);
box.visible = obj.visible && (source.unseenCount > 0);
this._updateSourceBoxStyle(source, obj, box);
}
_sourceAdded(tray, source, initial) {
let obj = {
visible: source.policy.showInLockScreen,
detailed: this._shouldShowDetails(source),
sourceDestroyId: 0,
sourceCountChangedId: 0,
sourceTitleChangedId: 0,
sourceUpdatedId: 0,
sourceBox: null,
titleLabel: null,
countLabel: null,
hasCriticalNotification: false,
};
obj.sourceBox = new St.BoxLayout({
style_class: 'unlock-dialog-notification-source',
x_expand: true,
});
this._showSource(source, obj, obj.sourceBox);
this._notificationBox.add_child(obj.sourceBox);
obj.sourceCountChangedId = source.connect('notify::count', () => {
this._countChanged(source, obj);
});
obj.sourceTitleChangedId = source.connect('notify::title', () => {
this._titleChanged(source, obj);
});
obj.policyChangedId = source.policy.connect('notify', (policy, pspec) => {
if (pspec.name === 'show-in-lock-screen')
this._visibleChanged(source, obj);
else
this._detailedChanged(source, obj);
});
obj.sourceDestroyId = source.connect('destroy', () => {
this._onSourceDestroy(source, obj);
});
this._sources.set(source, obj);
if (!initial) {
// block scrollbars while animating, if they're not needed now
let boxHeight = this._notificationBox.height;
if (this._scrollView.height >= boxHeight)
this._scrollView.vscrollbar_policy = St.PolicyType.NEVER;
let widget = obj.sourceBox;
let [, natHeight] = widget.get_preferred_height(-1);
widget.height = 0;
widget.ease({
height: natHeight,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
duration: 250,
onComplete: () => {
this._scrollView.vscrollbar_policy = St.PolicyType.AUTOMATIC;
widget.set_height(-1);
},
});
this._updateVisibility();
if (obj.sourceBox.visible)
this.emit('wake-up-screen');
}
}
_titleChanged(source, obj) {
obj.titleLabel.text = source.title;
}
_countChanged(source, obj) {
// A change in the number of notifications may change whether we show
// details.
let newDetailed = this._shouldShowDetails(source);
let oldDetailed = obj.detailed;
obj.detailed = newDetailed;
if (obj.detailed || oldDetailed !== newDetailed) {
// A new notification was pushed, or a previous notification was destroyed.
// Give up, and build the list again.
obj.sourceBox.destroy_all_children();
obj.titleLabel = obj.countLabel = null;
this._showSource(source, obj, obj.sourceBox);
} else {
let count = source.unseenCount;
obj.countLabel.text = `${count}`;
obj.countLabel.visible = count > 1;
}
obj.sourceBox.visible = obj.visible && (source.unseenCount > 0);
this._updateVisibility();
if (obj.sourceBox.visible)
this.emit('wake-up-screen');
}
_visibleChanged(source, obj) {
if (obj.visible === source.policy.showInLockScreen)
return;
obj.visible = source.policy.showInLockScreen;
obj.sourceBox.visible = obj.visible && source.unseenCount > 0;
this._updateVisibility();
if (obj.sourceBox.visible)
this.emit('wake-up-screen');
}
_detailedChanged(source, obj) {
let newDetailed = this._shouldShowDetails(source);
if (obj.detailed === newDetailed)
return;
obj.detailed = newDetailed;
obj.sourceBox.destroy_all_children();
obj.titleLabel = obj.countLabel = null;
this._showSource(source, obj, obj.sourceBox);
}
_onSourceDestroy(source, obj) {
this._removeSource(source, obj);
this._updateVisibility();
}
_removeSource(source, obj) {
obj.sourceBox.destroy();
obj.sourceBox = obj.titleLabel = obj.countLabel = null;
source.disconnect(obj.sourceDestroyId);
source.disconnect(obj.sourceCountChangedId);
source.disconnect(obj.sourceTitleChangedId);
source.policy.disconnect(obj.policyChangedId);
this._sources.delete(source);
}
});
var Clock = GObject.registerClass(
class UnlockDialogClock extends St.BoxLayout {
_init() {
super._init({ style_class: 'unlock-dialog-clock', vertical: true });
this._time = new St.Label({
style_class: 'unlock-dialog-clock-time',
x_align: Clutter.ActorAlign.CENTER,
});
this._date = new St.Label({
style_class: 'unlock-dialog-clock-date',
x_align: Clutter.ActorAlign.CENTER,
});
this._hint = new St.Label({
style_class: 'unlock-dialog-clock-hint',
x_align: Clutter.ActorAlign.CENTER,
opacity: 0,
});
this.add_child(this._time);
this.add_child(this._date);
this.add_child(this._hint);
this._wallClock = new GnomeDesktop.WallClock({ time_only: true });
this._wallClock.connect('notify::clock', this._updateClock.bind(this));
this._seat = Clutter.get_default_backend().get_default_seat();
this._touchModeChangedId = this._seat.connect('notify::touch-mode',
this._updateHint.bind(this));
this._monitorManager = Meta.MonitorManager.get();
this._powerModeChangedId = this._monitorManager.connect(
'power-save-mode-changed', () => (this._hint.opacity = 0));
this._idleMonitor = Meta.IdleMonitor.get_core();
this._idleWatchId = this._idleMonitor.add_idle_watch(HINT_TIMEOUT * 1000, () => {
this._hint.ease({
opacity: 255,
duration: CROSSFADE_TIME,
});
});
this._updateClock();
this._updateHint();
this.connect('destroy', this._onDestroy.bind(this));
}
_updateClock() {
this._time.text = this._wallClock.clock;
let date = new Date();
/* Translators: This is a time format for a date in
long format */
let dateFormat = Shell.util_translate_time_string(N_('%A %B %-d'));
this._date.text = date.toLocaleFormat(dateFormat);
}
_updateHint() {
this._hint.text = this._seat.touch_mode
? _('Swipe up to unlock')
: _('Click or press a key to unlock');
}
_onDestroy() {
this._wallClock.run_dispose();
this._seat.disconnect(this._touchModeChangedId);
this._idleMonitor.remove_watch(this._idleWatchId);
this._monitorManager.disconnect(this._powerModeChangedId);
}
});
var UnlockDialogLayout = GObject.registerClass(
class UnlockDialogLayout extends Clutter.LayoutManager {
_init(stack, notifications) {
super._init();
this._stack = stack;
this._notifications = notifications;
}
vfunc_get_preferred_width(container, forHeight) {
return this._stack.get_preferred_width(forHeight);
}
vfunc_get_preferred_height(container, forWidth) {
return this._stack.get_preferred_height(forWidth);
}
vfunc_allocate(container, box, flags) {
let [width, height] = box.get_size();
let tenthOfHeight = height / 10.0;
let thirdOfHeight = height / 3.0;
let [, , stackWidth, stackHeight] =
this._stack.get_preferred_size();
let [, , notificationsWidth, notificationsHeight] =
this._notifications.get_preferred_size();
let columnWidth = Math.max(stackWidth, notificationsWidth);
let columnX1 = Math.floor((width - columnWidth) / 2.0);
let actorBox = new Clutter.ActorBox();
// Notifications
let maxNotificationsHeight = Math.min(
notificationsHeight,
height - tenthOfHeight - stackHeight);
actorBox.x1 = columnX1;
actorBox.y1 = height - maxNotificationsHeight;
actorBox.x2 = columnX1 + columnWidth;
actorBox.y2 = actorBox.y1 + maxNotificationsHeight;
this._notifications.allocate(actorBox, flags);
// Authentication Box
let stackY = Math.min(
thirdOfHeight,
height - stackHeight - maxNotificationsHeight);
actorBox.x1 = columnX1;
actorBox.y1 = stackY;
actorBox.x2 = columnX1 + columnWidth;
actorBox.y2 = stackY + stackHeight;
this._stack.allocate(actorBox, flags);
}
});
var UnlockDialog = GObject.registerClass({
Signals: {
'failed': {},
'wake-up-screen': {},
},
}, class UnlockDialog extends St.Widget {
_init(parentActor) {
super._init({
accessible_role: Atk.Role.WINDOW,
style_class: 'login-dialog',
visible: false,
can_focus: true,
reactive: true,
});
parentActor.add_child(this);
this._adjustment = new St.Adjustment({
lower: 0,
upper: 2,
page_size: 1,
page_increment: 1,
});
this._adjustment.connect('notify::value', () => {
this._setTransitionProgress(this._adjustment.value);
});
this._swipeTracker = new SwipeTracker.SwipeTracker(
this, Shell.ActionMode.UNLOCK_SCREEN);
this._swipeTracker.connect('begin', this._swipeBegin.bind(this));
this._swipeTracker.connect('update', this._swipeUpdate.bind(this));
this._swipeTracker.connect('end', this._swipeEnd.bind(this));
this.connect('scroll-event', (o, event) => {
if (this._swipeTracker.canHandleScrollEvent(event))
return Clutter.EVENT_PROPAGATE;
let direction = event.get_scroll_direction();
if (direction === Clutter.ScrollDirection.UP)
this._showClock();
else if (direction === Clutter.ScrollDirection.DOWN)
this._showPrompt();
return Clutter.EVENT_STOP;
});
this._activePage = null;
let tapAction = new Clutter.TapAction();
tapAction.connect('tap', this._showPrompt.bind(this));
this.add_action(tapAction);
// Background
this._backgroundGroup = new Clutter.Actor();
this.add_child(this._backgroundGroup);
this._bgManagers = [];
this._updateBackgrounds();
this._monitorsChangedId =
Main.layoutManager.connect('monitors-changed', this._updateBackgrounds.bind(this));
this._userManager = AccountsService.UserManager.get_default();
this._userName = GLib.get_user_name();
this._user = this._userManager.get_user(this._userName);
// Authentication & Clock stack
this._stack = new Shell.Stack();
this._promptBox = new St.BoxLayout({ vertical: true });
this._promptBox.set_pivot_point(0.5, 0.5);
this._promptBox.hide();
this._stack.add_child(this._promptBox);
this._clock = new Clock();
this._clock.set_pivot_point(0.5, 0.5);
this._stack.add_child(this._clock);
this._showClock();
this.allowCancel = false;
Main.ctrlAltTabManager.addGroup(this, _('Unlock Window'), 'dialog-password-symbolic');
// Notifications
this._notificationsBox = new NotificationsBox();
this._notificationsBox.connect('wake-up-screen', () => this.emit('wake-up-screen'));
// Main Box
let mainBox = new Clutter.Actor();
mainBox.add_constraint(new Layout.MonitorConstraint({ primary: true }));
mainBox.add_child(this._stack);
mainBox.add_child(this._notificationsBox);
mainBox.layout_manager = new UnlockDialogLayout(
this._stack,
this._notificationsBox);
this.add_child(mainBox);
this._idleMonitor = Meta.IdleMonitor.get_core();
this._idleWatchId = this._idleMonitor.add_idle_watch(IDLE_TIMEOUT * 1000, this._escape.bind(this));
this.connect('destroy', this._onDestroy.bind(this));
}
vfunc_key_press_event(keyEvent) {
if (this._activePage === this._promptBox ||
(this._promptBox && this._promptBox.visible))
return Clutter.EVENT_PROPAGATE;
let unichar = keyEvent.unicode_value;
this._showPrompt();
if (GLib.unichar_isgraph(unichar))
this.addCharacter(unichar);
return Clutter.EVENT_PROPAGATE;
}
_createBackground(monitorIndex) {
let monitor = Main.layoutManager.monitors[monitorIndex];
let widget = new St.Widget({
style_class: 'screen-shield-background',
x: monitor.x,
y: monitor.y,
width: monitor.width,
height: monitor.height,
});
let bgManager = new Background.BackgroundManager({
container: widget,
monitorIndex,
controlPosition: false,
settingsSchema: SCREENSAVER_SCHEMA,
});
this._bgManagers.push(bgManager);
this._backgroundGroup.add_child(widget);
const themeContext = St.ThemeContext.get_for_stage(global.stage);
let effect = new Shell.BlurEffect({
brightness: BLUR_BRIGHTNESS,
sigma: BLUR_SIGMA * themeContext.scale_factor,
});
this._scaleChangedId = themeContext.connect('notify::scale-factor', () => {
effect.sigma = BLUR_SIGMA * themeContext.scale_factor;
});
widget.add_effect(effect);
}
_updateBackgrounds() {
for (let i = 0; i < this._bgManagers.length; i++)
this._bgManagers[i].destroy();
this._bgManagers = [];
this._backgroundGroup.destroy_all_children();
for (let i = 0; i < Main.layoutManager.monitors.length; i++)
this._createBackground(i);
}
_ensureAuthPrompt() {
if (this._authPrompt)
return;
this._authPrompt = new AuthPrompt.AuthPrompt(new Gdm.Client(), AuthPrompt.AuthPromptMode.UNLOCK_ONLY);
this._authPrompt.connect('failed', this._fail.bind(this));
this._authPrompt.connect('cancelled', this._fail.bind(this));
this._authPrompt.connect('reset', this._onReset.bind(this));
this._promptBox.add_child(this._authPrompt);
this._authPrompt.reset();
this._updateSensitivity(true);
}
_maybeDestroyAuthPrompt() {
let focus = global.stage.key_focus;
if (focus === null ||
(this._authPrompt && this._authPrompt.contains(focus)))
this.grab_key_focus();
if (this._authPrompt) {
this._authPrompt.destroy();
this._authPrompt = null;
}
}
_updateSensitivity(sensitive) {
this._authPrompt.updateSensitivity(sensitive);
}
_showClock() {
if (this._activePage === this._clock)
return;
this._activePage = this._clock;
this._adjustment.ease(0, {
duration: CROSSFADE_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => this._maybeDestroyAuthPrompt(),
});
}
_showPrompt() {
this._ensureAuthPrompt();
if (this._activePage === this._promptBox)
return;
this._activePage = this._promptBox;
this._promptBox.show();
this._adjustment.ease(1, {
duration: CROSSFADE_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
}
_setTransitionProgress(progress) {
this._promptBox.visible = progress > 0;
this._clock.visible = progress < 1;
const { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
this._promptBox.set({
opacity: 255 * progress,
scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * progress,
translation_y: FADE_OUT_TRANSLATION * (1 - progress) * scaleFactor,
});
this._clock.set({
opacity: 255 * (1 - progress),
scale_x: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * (1 - progress),
scale_y: FADE_OUT_SCALE + (1 - FADE_OUT_SCALE) * (1 - progress),
translation_y: -FADE_OUT_TRANSLATION * progress * scaleFactor,
});
}
_fail() {
this._showClock();
this.emit('failed');
}
_onReset(authPrompt, beginRequest) {
let userName;
if (beginRequest == AuthPrompt.BeginRequestType.PROVIDE_USERNAME) {
this._authPrompt.setUser(this._user);
userName = this._userName;
} else {
userName = null;
}
this._authPrompt.begin({ userName });
}
_escape() {
if (this.allowCancel)
this._authPrompt.cancel();
}
_swipeBegin(tracker, monitor) {
if (monitor !== Main.layoutManager.primaryIndex)
return;
this._adjustment.remove_transition('value');
this._ensureAuthPrompt();
let progress = this._adjustment.value;
tracker.confirmSwipe(this._stack.height,
[0, 1],
progress,
Math.round(progress));
}
_swipeUpdate(tracker, progress) {
this._adjustment.value = progress;
}
_swipeEnd(tracker, duration, endProgress) {
this._activePage = endProgress
? this._promptBox
: this._clock;
this._adjustment.ease(endProgress, {
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
duration,
onComplete: () => {
if (this._activePage === this._clock)
this._maybeDestroyAuthPrompt();
},
});
}
_onDestroy() {
this.popModal();
if (this._idleWatchId) {
this._idleMonitor.remove_watch(this._idleWatchId);
this._idleWatchId = 0;
}
if (this._monitorsChangedId) {
Main.layoutManager.disconnect(this._monitorsChangedId);
delete this._monitorsChangedId;
}
let themeContext = St.ThemeContext.get_for_stage(global.stage);
if (this._scaleChangedId) {
themeContext.disconnect(this._scaleChangedId);
delete this._scaleChangedId;
}
}
cancel() {
if (this._authPrompt)
this._authPrompt.cancel();
}
addCharacter(unichar) {
this._showPrompt();
this._authPrompt.addCharacter(unichar);
}
finish(onComplete) {
this._ensureAuthPrompt();
this._authPrompt.finish(onComplete);
}
open(timestamp) {
this.show();
if (this._isModal)
return true;
let modalParams = {
timestamp,
actionMode: Shell.ActionMode.UNLOCK_SCREEN,
};
if (!Main.pushModal(this, modalParams))
return false;
this._isModal = true;
return true;
}
activate() {
this._showPrompt();
}
popModal(timestamp) {
if (this._isModal) {
Main.popModal(this, timestamp);
this._isModal = false;
}
}
});