gnome-shell/js/ui/userWidget.js
Jonas Dreßler 3fb0284358 Make sure to allocate all children in allocate vfuncs
Clutter expects actors overriding the allocate vfunc to allocate all
mapped children of the actor, otherwise bad things happen.

So make sure we actually allocate all our visible children in our custom
allocation functions, and since we don't want to give them a real
allocation, just pass them an empty ClutterActorBox.

It would be nice if we had a way to hide children during the allocation
process where no relayout is queued like gtk allows with
gtk_widget_set_child_visible(), then we could avoid those weird empty
ClutterActorBoxes.

Fixes https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/3098

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1481>
2020-11-24 20:20:45 +00:00

250 lines
7.8 KiB
JavaScript

// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
// A widget showing the user avatar and name
/* exported UserWidget */
const { Clutter, GLib, GObject, St } = imports.gi;
const Params = imports.misc.params;
var AVATAR_ICON_SIZE = 64;
// Adapted from gdm/gui/user-switch-applet/applet.c
//
// Copyright (C) 2004-2005 James M. Cape <jcape@ignore-your.tv>.
// Copyright (C) 2008,2009 Red Hat, Inc.
var Avatar = GObject.registerClass(
class Avatar extends St.Bin {
_init(user, params) {
let themeContext = St.ThemeContext.get_for_stage(global.stage);
params = Params.parse(params, {
styleClass: 'user-icon',
reactive: false,
iconSize: AVATAR_ICON_SIZE,
});
super._init({
style_class: params.styleClass,
reactive: params.reactive,
width: params.iconSize * themeContext.scaleFactor,
height: params.iconSize * themeContext.scaleFactor,
});
this._iconSize = params.iconSize;
this._user = user;
this.bind_property('reactive', this, 'track-hover',
GObject.BindingFlags.SYNC_CREATE);
this.bind_property('reactive', this, 'can-focus',
GObject.BindingFlags.SYNC_CREATE);
// Monitor the scaling factor to make sure we recreate the avatar when needed.
this._scaleFactorChangeId =
themeContext.connect('notify::scale-factor', this.update.bind(this));
this.connect('destroy', this._onDestroy.bind(this));
}
vfunc_style_changed() {
super.vfunc_style_changed();
let node = this.get_theme_node();
let [found, iconSize] = node.lookup_length('icon-size', false);
if (!found)
return;
let themeContext = St.ThemeContext.get_for_stage(global.stage);
// node.lookup_length() returns a scaled value, but we
// need unscaled
this._iconSize = iconSize / themeContext.scaleFactor;
this.update();
}
_onDestroy() {
if (this._scaleFactorChangeId) {
let themeContext = St.ThemeContext.get_for_stage(global.stage);
themeContext.disconnect(this._scaleFactorChangeId);
delete this._scaleFactorChangeId;
}
}
setSensitive(sensitive) {
this.reactive = sensitive;
}
update() {
let iconFile = null;
if (this._user) {
iconFile = this._user.get_icon_file();
if (iconFile && !GLib.file_test(iconFile, GLib.FileTest.EXISTS))
iconFile = null;
}
let { scaleFactor } = St.ThemeContext.get_for_stage(global.stage);
this.set_size(
this._iconSize * scaleFactor,
this._iconSize * scaleFactor);
if (iconFile) {
this.child = null;
this.style = `
background-image: url("${iconFile}");
background-size: cover;`;
} else {
this.style = null;
this.child = new St.Icon({
icon_name: 'avatar-default-symbolic',
icon_size: this._iconSize,
});
}
}
});
var UserWidgetLabel = GObject.registerClass(
class UserWidgetLabel extends St.Widget {
_init(user) {
super._init({ layout_manager: new Clutter.BinLayout() });
this._user = user;
this._realNameLabel = new St.Label({ style_class: 'user-widget-label',
y_align: Clutter.ActorAlign.CENTER });
this.add_child(this._realNameLabel);
this._userNameLabel = new St.Label({ style_class: 'user-widget-label',
y_align: Clutter.ActorAlign.CENTER });
this.add_child(this._userNameLabel);
this._currentLabel = null;
this._userLoadedId = this._user.connect('notify::is-loaded', this._updateUser.bind(this));
this._userChangedId = this._user.connect('changed', this._updateUser.bind(this));
this._updateUser();
// We can't override the destroy vfunc because that might be called during
// object finalization, and we can't call any JS inside a GC finalize callback,
// so we use a signal, that will be disconnected by GObject the first time
// the actor is destroyed (which is guaranteed to be as part of a normal
// destroy() call from JS, possibly from some ancestor)
this.connect('destroy', this._onDestroy.bind(this));
}
_onDestroy() {
if (this._userLoadedId != 0) {
this._user.disconnect(this._userLoadedId);
this._userLoadedId = 0;
}
if (this._userChangedId != 0) {
this._user.disconnect(this._userChangedId);
this._userChangedId = 0;
}
}
vfunc_allocate(box) {
this.set_allocation(box);
let availWidth = box.x2 - box.x1;
let availHeight = box.y2 - box.y1;
let [, , natRealNameWidth] = this._realNameLabel.get_preferred_size();
let childBox = new Clutter.ActorBox();
let hiddenLabel;
if (natRealNameWidth <= availWidth) {
this._currentLabel = this._realNameLabel;
hiddenLabel = this._userNameLabel;
} else {
this._currentLabel = this._userNameLabel;
hiddenLabel = this._realNameLabel;
}
this.label_actor = this._currentLabel;
hiddenLabel.allocate(childBox);
childBox.set_size(availWidth, availHeight);
this._currentLabel.allocate(childBox);
}
vfunc_paint(paintContext) {
this._currentLabel.paint(paintContext);
}
_updateUser() {
if (this._user.is_loaded) {
this._realNameLabel.text = this._user.get_real_name();
this._userNameLabel.text = this._user.get_user_name();
} else {
this._realNameLabel.text = '';
this._userNameLabel.text = '';
}
}
});
var UserWidget = GObject.registerClass(
class UserWidget extends St.BoxLayout {
_init(user, orientation = Clutter.Orientation.HORIZONTAL) {
// If user is null, that implies a username-based login authorization.
this._user = user;
let vertical = orientation == Clutter.Orientation.VERTICAL;
let xAlign = vertical ? Clutter.ActorAlign.CENTER : Clutter.ActorAlign.START;
let styleClass = vertical ? 'user-widget vertical' : 'user-widget horizontal';
super._init({
styleClass,
vertical,
xAlign,
});
this.connect('destroy', this._onDestroy.bind(this));
this._avatar = new Avatar(user);
this._avatar.x_align = Clutter.ActorAlign.CENTER;
this.add_child(this._avatar);
this._userLoadedId = 0;
this._userChangedId = 0;
if (user) {
this._label = new UserWidgetLabel(user);
this.add_child(this._label);
this._label.bind_property('label-actor', this, 'label-actor',
GObject.BindingFlags.SYNC_CREATE);
this._userLoadedId = this._user.connect('notify::is-loaded', this._updateUser.bind(this));
this._userChangedId = this._user.connect('changed', this._updateUser.bind(this));
} else {
this._label = new St.Label({
style_class: 'user-widget-label',
text: 'Empty User',
opacity: 0,
});
this.add_child(this._label);
}
this._updateUser();
}
_onDestroy() {
if (this._userLoadedId != 0) {
this._user.disconnect(this._userLoadedId);
this._userLoadedId = 0;
}
if (this._userChangedId != 0) {
this._user.disconnect(this._userChangedId);
this._userChangedId = 0;
}
}
_updateUser() {
this._avatar.update();
}
});