messageList: Center expanded notification group in view

Ensure that when the user expends a group, it is scrolled to and centered
in the in the `MessageListView`.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/3012>
This commit is contained in:
Julian Sparber 2023-12-14 16:25:25 +01:00 committed by Marge Bot
parent aac7c125ce
commit a795dfb2ff

View File

@ -18,6 +18,7 @@ import {formatTimeSpan} from '../misc/dateUtils.js';
const MAX_NOTIFICATION_BUTTONS = 3; const MAX_NOTIFICATION_BUTTONS = 3;
const MESSAGE_ANIMATION_TIME = 100; const MESSAGE_ANIMATION_TIME = 100;
const EXPANDED_GROUP_OVERSHOT_HEIGHT = 50;
const DEFAULT_EXPAND_LINES = 6; const DEFAULT_EXPAND_LINES = 6;
const GROUP_EXPENSION_TIME = 200; const GROUP_EXPENSION_TIME = 200;
@ -987,6 +988,11 @@ const NotificationMessageGroup = GObject.registerClass({
this._headerBox.hide(); this._headerBox.hide();
} }
get expandedHeight() {
const [min] = this.layoutManager.getExpandedHeight(this, -1);
return min;
}
// Ensure that the cover is still below the top most message // Ensure that the cover is still below the top most message
_ensureCoverPosition() { _ensureCoverPosition() {
// If the group doesn't have any messages, // If the group doesn't have any messages,
@ -1456,6 +1462,7 @@ export const MessageView = GObject.registerClass({
Signals: { Signals: {
'message-focused': {param_types: [Message]}, 'message-focused': {param_types: [Message]},
}, },
Implements: [St.Scrollable],
}, class MessageView extends St.Viewport { }, class MessageView extends St.Viewport {
messages = []; messages = [];
@ -1643,6 +1650,92 @@ export const MessageView = GObject.registerClass({
} }
} }
// When a group is expanded the user isn't allowed to scroll outside the expanded group,
// therefore the adjustment used by the MessageView needs to be different then the external
// adjustment used by the scrollbar and scrollview.
vfunc_set_adjustments(hadjustment, vadjustment) {
const internalAdjustment = new St.Adjustment({actor: vadjustment.actor});
this._scrollViewAdjustment = vadjustment;
this._adjValueOffset = 0;
this._adjBinding = new GObject.BindingGroup();
this._adjBinding.bind('lower',
this._scrollViewAdjustment,
'lower',
GObject.BindingFlags.SYNC_CREATE);
this._adjBinding.bind('upper',
this._scrollViewAdjustment,
'upper', GObject.BindingFlags.SYNC_CREATE);
this._adjBinding.bind('step-increment',
this._scrollViewAdjustment, 'step-increment',
GObject.BindingFlags.SYNC_CREATE);
this._adjBinding.bind('page-increment',
this._scrollViewAdjustment, 'page-increment',
GObject.BindingFlags.SYNC_CREATE);
this._adjBinding.bind('page-size',
this._scrollViewAdjustment, 'page-size',
GObject.BindingFlags.SYNC_CREATE);
internalAdjustment.bind_property_full('value',
this._scrollViewAdjustment,
'value',
GObject.BindingFlags.BIDIRECTIONAL,
(bind, value) => {
return [true, value - this._adjValueOffset];
},
(bind, value) => {
return [true, value + this._adjValueOffset];
});
super.vfunc_set_adjustments(hadjustment, internalAdjustment);
}
vfunc_allocate(box) {
const group = this.expandedGroup;
this.vadjustment.freeze_notify();
const prevUpper = this.vadjustment.upper;
const prevAdjValueOffset = this._adjValueOffset;
super.vfunc_allocate(box);
if (group) {
// Decouple the internal adjustment from the external when there is an expanded group
this._adjBinding.set_source(null);
const pageHeight = this.vadjustment.pageSize;
const position = group.apply_relative_transform_to_point(this, new Graphene.Point3D());
const [groupHeight] = group.get_preferred_height(box.x2 - box.x1);
this._adjValueOffset = Math.max(0, position.y - EXPANDED_GROUP_OVERSHOT_HEIGHT);
// Limit the area the user can scroll to the expanded group with some extra space
this._scrollViewAdjustment.freeze_notify();
this._scrollViewAdjustment.upper =
Math.max(groupHeight + EXPANDED_GROUP_OVERSHOT_HEIGHT * 2, pageHeight);
this._scrollViewAdjustment.stepIncrement = pageHeight / 6;
this._scrollViewAdjustment.pageIncrement = pageHeight - pageHeight / 6;
this._scrollViewAdjustment.pageSize = pageHeight;
this._scrollViewAdjustment.thaw_notify();
// Adjust the value when new messages are added before the expanded group
if (this._adjValueOffset > prevAdjValueOffset) {
const offset = this.vadjustment.upper - prevUpper;
if (offset > 0)
this.vadjustment.value += offset;
}
} else if (this._adjBinding.source === null) {
this._adjValueOffset = 0;
this._adjBinding.set_source(this.vadjustment);
// We need to notify the 'value' property since it indirectly changed
this.vadjustment.notify('value');
}
this.vadjustment.thaw_notify();
}
_setupMpris() { _setupMpris() {
this._mediaSource.connectObject( this._mediaSource.connectObject(
'player-added', (_, player) => this._addPlayer(player), 'player-added', (_, player) => this._addPlayer(player),
@ -1722,6 +1815,25 @@ export const MessageView = GObject.registerClass({
this._notificationSourceToGroup.delete(source); this._notificationSourceToGroup.delete(source);
} }
// Try to center the expanded group in the available space
_scrollToExpandedGroup() {
if (!this._expandedGroup)
return;
const group = this._expandedGroup;
const groupExpandedHeight = group.expandedHeight;
const position = group.apply_relative_transform_to_point(this, new Graphene.Point3D());
const groupCenter = position.y + (groupExpandedHeight / 2);
const pageHeight = this.vadjustment.pageSize;
const pageCenter = pageHeight / 2;
const value = Math.min(position.y, groupCenter - pageCenter);
this.vadjustment.ease(value, {
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
duration: GROUP_EXPENSION_TIME,
});
}
get expandedGroup() { get expandedGroup() {
return this._expandedGroup; return this._expandedGroup;
} }
@ -1743,6 +1855,8 @@ export const MessageView = GObject.registerClass({
this.set_child_above_sibling(group.get_parent(), null); this.set_child_above_sibling(group.get_parent(), null);
this.set_child_below_sibling(this._overlay, group.get_parent()); this.set_child_below_sibling(this._overlay, group.get_parent());
this._overlay.show(); this._overlay.show();
this._scrollToExpandedGroup();
await group.expand(); await group.expand();
} else { } else {
this._overlay.hide(); this._overlay.hide();