3fb0284358
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>
250 lines
7.8 KiB
JavaScript
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();
|
|
}
|
|
});
|