Skip to content

Commit

Permalink
Add userWidget.js (#12084)
Browse files Browse the repository at this point in the history
Ported from Gnome Shell as a convenient widget to use in the new Polkit
dialog. Also use in the user applet and can be used in other places like a
new screensaver implementation.
  • Loading branch information
JosephMcc committed Apr 5, 2024
1 parent 3c14e5b commit 5bbfa2e
Show file tree
Hide file tree
Showing 4 changed files with 268 additions and 24 deletions.
5 changes: 5 additions & 0 deletions data/theme/cinnamon.css
Original file line number Diff line number Diff line change
Expand Up @@ -2185,3 +2185,8 @@ StScrollBar StButton#vhandle:hover {
-pie-border-color: rgba(200, 200, 200, 0.8);
-pie-background-color: rgba(140, 140, 140, 0.6);;
}

.user-icon {
border: 2px solid #868686;
border-radius: 99px;
}
23 changes: 9 additions & 14 deletions files/usr/share/cinnamon/applets/[email protected]/applet.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const AccountsService = imports.gi.AccountsService;
const GnomeSession = imports.misc.gnomeSession;
const ScreenSaver = imports.misc.screenSaver;
const Settings = imports.ui.settings;
const UserWidget = imports.ui.userWidget;

const DIALOG_ICON_SIZE = 64;

class CinnamonUserApplet extends Applet.TextIconApplet {
constructor(orientation, panel_height, instance_id) {
Expand All @@ -28,9 +31,13 @@ class CinnamonUserApplet extends Applet.TextIconApplet {
this._contentSection = new PopupMenu.PopupMenuSection();
this.menu.addMenuItem(this._contentSection);

this._user = AccountsService.UserManager.get_default().get_user(GLib.get_user_name());
this._userLoadedId = this._user.connect('notify::is-loaded', Lang.bind(this, this._onUserChanged));
this._userChangedId = this._user.connect('changed', Lang.bind(this, this._onUserChanged));

let userBox = new St.BoxLayout({ style_class: 'user-box', reactive: true, vertical: false });

this._userIcon = new St.Bin({ style_class: 'user-icon'});
this._userIcon = new UserWidget.Avatar(this._user, { iconSize: DIALOG_ICON_SIZE });

this.settings.bind("display-name", "disp_name", this._updateLabel);

Expand Down Expand Up @@ -126,9 +133,6 @@ class CinnamonUserApplet extends Applet.TextIconApplet {
}));
this.menu.addMenuItem(item);

this._user = AccountsService.UserManager.get_default().get_user(GLib.get_user_name());
this._userLoadedId = this._user.connect('notify::is-loaded', Lang.bind(this, this._onUserChanged));
this._userChangedId = this._user.connect('changed', Lang.bind(this, this._onUserChanged));
this._onUserChanged();
this.set_show_label_in_vertical_panels(false);
}
Expand All @@ -150,16 +154,7 @@ class CinnamonUserApplet extends Applet.TextIconApplet {
this.set_applet_tooltip(this._user.get_real_name());
this.userLabel.set_text (this._user.get_real_name());
if (this._userIcon) {
let iconFileName = this._user.get_icon_file();
let iconFile = Gio.file_new_for_path(iconFileName);
let icon;
if (iconFile.query_exists(null)) {
icon = new Gio.FileIcon({file: iconFile});
} else {
icon = new Gio.ThemedIcon({name: 'avatar-default'});
}
let img = St.TextureCache.get_default().load_gicon(null, icon, 48);
this._userIcon.set_child (img);
this._userIcon.update();
this._userIcon.show();
}
this._updateLabel();
Expand Down
15 changes: 5 additions & 10 deletions js/ui/polkitAuthenticationAgent.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const PolkitAgent = imports.gi.PolkitAgent;

const ModalDialog = imports.ui.modalDialog;
const CinnamonEntry = imports.ui.cinnamonEntry;
const UserWidget = imports.ui.userWidget;

const DIALOG_ICON_SIZE = 64;

function AuthenticationDialog(actionId, message, cookie, userNames) {
this._init(actionId, message, cookie, userNames);
Expand Down Expand Up @@ -103,7 +106,7 @@ AuthenticationDialog.prototype = {
let userBox = new St.BoxLayout({ style_class: 'polkit-dialog-user-layout',
vertical: true });
mainContentBox.add(userBox);
this._userIcon = new St.Icon();
this._userIcon = new UserWidget.Avatar(this._user, { iconSize: DIALOG_ICON_SIZE });
this._userIcon.hide();
userBox.add(this._userIcon,
{ x_fill: false,
Expand Down Expand Up @@ -301,15 +304,7 @@ AuthenticationDialog.prototype = {
_onUserChanged: function() {
if (this._user.is_loaded) {
if (this._userIcon) {
let iconFileName = this._user.get_icon_file();
let iconFile = Gio.file_new_for_path(iconFileName);
let icon;
if (iconFile.query_exists(null)) {
icon = new Gio.FileIcon({file: iconFile});
} else {
icon = new Gio.ThemedIcon({name: 'avatar-default'});
}
this._userIcon.set_gicon (icon);
this._userIcon.update();
this._userIcon.show();
}
}
Expand Down
249 changes: 249 additions & 0 deletions js/ui/userWidget.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
//
// A widget showing the user avatar and name
/* exported UserWidget */

const Clutter = imports.gi.Clutter;
const GLib = imports.gi.GLib;
const GObject = imports.gi.GObject;
const St = imports.gi.St;

const Params = imports.misc.params;

var AVATAR_ICON_SIZE = 64;

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.set_important(true);
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.add_style_class_name('user-avatar');
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();
}
});

0 comments on commit 5bbfa2e

Please sign in to comment.