appDisplay: Use custom layout manager for page indicators

This is a major departure to how page previews used to work. Add
a new layout manager that handles showing and hiding page previews
and navigation arrows. Move most of the code handling page previews
to this new layout manager.

The layout manager allocates at most 20% of the screen width for
the previews, and at least the width of the arrow.

The next and previous page peeking effect is temporarily lost with
this commit, but will be reintroduced in later commits.

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2335>
This commit is contained in:
Georges Basile Stavracas Neto 2022-06-10 18:30:26 -03:00 committed by Marge Bot
parent b9a373c1ab
commit 6c00e0fda4

View File

@ -40,9 +40,8 @@ var APP_ICON_TITLE_COLLAPSE_TIME = 100;
const FOLDER_DIALOG_ANIMATION_TIME = 200;
const PAGE_PREVIEW_ANIMATION_TIME = 150;
const PAGE_PREVIEW_ANIMATION_START_OFFSET = 100;
const PAGE_PREVIEW_MAX_ARROW_OFFSET = 80;
const PAGE_INDICATOR_FADE_TIME = 200;
const PAGE_PREVIEW_RATIO = 0.20;
const OVERSHOOT_THRESHOLD = 20;
const OVERSHOOT_TIMEOUT = 1000;
@ -203,6 +202,194 @@ const AppGrid = GObject.registerClass({
}
});
const BaseAppViewGridLayout = GObject.registerClass(
class BaseAppViewGridLayout extends Clutter.BinLayout {
_init(grid, scrollView, nextPageIndicator, nextPageArrow,
previousPageIndicator, previousPageArrow) {
if (!(grid instanceof AppGrid))
throw new Error('Grid must be an AppGrid subclass');
super._init();
this._grid = grid;
this._scrollView = scrollView;
this._previousPageIndicator = previousPageIndicator;
this._previousPageArrow = previousPageArrow;
this._nextPageIndicator = nextPageIndicator;
this._nextPageArrow = nextPageArrow;
grid.connect('pages-changed', () => this._syncPageIndicatorsVisibility());
this._pageIndicatorsAdjustment = new St.Adjustment({
lower: 0,
upper: 1,
});
this._pageIndicatorsAdjustment.connect(
'notify::value', () => this._syncPageIndicators());
this._showIndicators = false;
this._currentPage = 0;
}
_getIndicatorsWidth(box) {
const [width, height] = box.get_size();
const arrows = [
this._nextPageArrow,
this._previousPageArrow,
];
const minArrowsWidth = arrows.reduce(
(previousWidth, accessory) => {
const [min] = accessory.get_preferred_width(height);
return Math.max(previousWidth, min);
}, 0);
const idealIndicatorWidth = (width * PAGE_PREVIEW_RATIO) / 2;
return Math.max(idealIndicatorWidth, minArrowsWidth);
}
_syncPageIndicatorsVisibility(animate = true) {
const previousIndicatorsVisible =
this._currentPage > 0 && this._showIndicators;
if (previousIndicatorsVisible) {
this._previousPageIndicator.show();
this._previousPageArrow.show();
}
this._previousPageIndicator.ease({
opacity: previousIndicatorsVisible ? 255 : 0,
duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
onComplete: () => {
if (!previousIndicatorsVisible) {
this._previousPageIndicator.hide();
this._previousPageArrow.hide();
}
},
});
this._previousPageArrow.ease({
opacity: previousIndicatorsVisible ? 255 : 0,
duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
});
// Always show the next page indicator to allow dropping
// icons into new pages
const {allowIncompletePages, nPages} = this._grid.layoutManager;
const nextIndicatorsVisible = this._showIndicators &&
(allowIncompletePages ? true : this._currentPage < nPages - 1);
if (nextIndicatorsVisible) {
this._nextPageIndicator.show();
this._nextPageArrow.show();
}
this._nextPageIndicator.ease({
opacity: nextIndicatorsVisible ? 255 : 0,
duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
onComplete: () => {
if (!nextIndicatorsVisible) {
this._nextPageIndicator.hide();
this._nextPageArrow.hide();
}
},
});
this._nextPageArrow.ease({
opacity: nextIndicatorsVisible ? 255 : 0,
duration: animate ? PAGE_INDICATOR_FADE_TIME : 0,
});
}
_syncPageIndicators() {
if (!this._container)
return;
const {value} = this._pageIndicatorsAdjustment;
const ltr = this._container.get_text_direction() !== Clutter.TextDirection.RTL;
const {left, right} = this._grid.indicatorsPadding;
const leftOffset = -left * (1 - value);
const rightOffset = right * (1 - value);
const leftArrowOffset = -left * value;
const rightArrowOffset = right * value;
this._previousPageArrow.translationX =
ltr ? leftArrowOffset : rightArrowOffset;
this._nextPageArrow.translationX =
ltr ? rightArrowOffset : leftArrowOffset;
this._previousPageIndicator.translationX = ltr ? leftOffset : rightOffset;
this._previousPageArrow.translationX = ltr ? leftOffset : rightOffset;
this._nextPageIndicator.translationX = ltr ? rightOffset : leftOffset;
this._nextPageArrow.translationX = ltr ? rightOffset : leftOffset;
}
vfunc_set_container(container) {
this._container = container;
this._pageIndicatorsAdjustment.actor = container;
this._syncPageIndicators();
}
vfunc_allocate(container, box) {
const ltr = container.get_text_direction() !== Clutter.TextDirection.RTL;
const indicatorsWidth = this._getIndicatorsWidth(box);
this._grid.indicatorsPadding = new Clutter.Margin({
left: indicatorsWidth,
right: indicatorsWidth,
});
this._scrollView.allocate(box);
const leftBox = box.copy();
leftBox.x2 = leftBox.x1 + indicatorsWidth;
const rightBox = box.copy();
rightBox.x1 = rightBox.x2 - indicatorsWidth;
this._previousPageIndicator.allocate(ltr ? leftBox : rightBox);
this._previousPageArrow.allocate(ltr ? leftBox : rightBox);
this._nextPageIndicator.allocate(ltr ? rightBox : leftBox);
this._nextPageArrow.allocate(ltr ? rightBox : leftBox);
}
goToPage(page, animate = true) {
if (this._currentPage === page)
return;
this._currentPage = page;
this._syncPageIndicatorsVisibility(animate);
this._syncPageIndicators();
}
showPageIndicators() {
if (this._showIndicators)
return;
this._pageIndicatorsAdjustment.ease(1, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
});
this._grid.clipToView = false;
this._showIndicators = true;
this._syncPageIndicatorsVisibility();
}
hidePageIndicators() {
if (!this._showIndicators)
return;
this._pageIndicatorsAdjustment.ease(0, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
onComplete: () => {
this._grid.clipToView = true;
},
});
this._showIndicators = false;
this._syncPageIndicatorsVisibility();
}
});
var BaseAppView = GObject.registerClass({
GTypeFlags: GObject.TypeFlags.ABSTRACT,
Properties: {
@ -238,14 +425,10 @@ var BaseAppView = GObject.registerClass({
enable_mouse_scrolling: false,
});
this._scrollView.set_policy(St.PolicyType.EXTERNAL, St.PolicyType.NEVER);
this._scrollView._delegate = this;
this._canScroll = true; // limiting scrolling speed
this._scrollTimeoutId = 0;
this._scrollView.connect('scroll-event', this._onScroll.bind(this));
this._scrollView.connect('motion-event', this._onMotion.bind(this));
this._scrollView.connect('enter-event', this._onMotion.bind(this));
this._scrollView.connect('leave-event', this._onLeave.bind(this));
this._scrollView.connect('button-press-event', this._onButtonPress.bind(this));
this._scrollView.add_actor(this._grid);
@ -255,20 +438,6 @@ var BaseAppView = GObject.registerClass({
this._adjustment.connect('notify::value', adj => {
const value = adj.value / adj.page_size;
this._pageIndicators.setCurrentPosition(value);
const distanceToPage = Math.abs(Math.round(value) - value);
if (distanceToPage < 0.001) {
this._hintContainer.opacity = 255;
this._hintContainer.translationX = 0;
} else {
this._hintContainer.remove_transition('opacity');
let opacity = Math.clamp(
255 * (1 - (distanceToPage * 2)),
0, 255);
this._hintContainer.translationX = (Math.round(value) - value) * adj.page_size;
this._hintContainer.opacity = opacity;
}
});
// Page Indicators
@ -332,23 +501,24 @@ var BaseAppView = GObject.registerClass({
x_align: Clutter.ActorAlign.START,
});
this._hintContainer = new St.Widget({
layout_manager: new Clutter.BinLayout(),
x_expand: true,
y_expand: true,
});
this._hintContainer.add_child(this._prevPageIndicator);
this._hintContainer.add_child(this._nextPageIndicator);
const scrollContainer = new St.Widget({
layout_manager: new Clutter.BinLayout(),
clip_to_allocation: true,
y_expand: true,
});
scrollContainer.add_child(this._hintContainer);
scrollContainer.add_child(this._scrollView);
scrollContainer.add_child(this._prevPageIndicator);
scrollContainer.add_child(this._nextPageIndicator);
scrollContainer.add_child(this._nextPageArrow);
scrollContainer.add_child(this._prevPageArrow);
scrollContainer.layoutManager = new BaseAppViewGridLayout(
this._grid,
this._scrollView,
this._nextPageIndicator,
this._nextPageArrow,
this._prevPageIndicator,
this._prevPageArrow);
this._appGridLayout = scrollContainer.layoutManager;
scrollContainer._delegate = this;
this._box = new St.BoxLayout({
vertical: true,
@ -460,27 +630,20 @@ var BaseAppView = GObject.registerClass({
_pageForCoords(x, y) {
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
const { allocation } = this._grid;
const {pagePadding} = this._grid.layoutManager;
const [success, pointerX] = this._scrollView.transform_stage_point(x, y);
if (!success)
return SidePages.NONE;
if (pointerX < allocation.x1)
if (pointerX < pagePadding.left)
return rtl ? SidePages.NEXT : SidePages.PREVIOUS;
else if (pointerX > allocation.x2)
else if (pointerX > this._scrollView.width - pagePadding.right)
return rtl ? SidePages.PREVIOUS : SidePages.NEXT;
return SidePages.NONE;
}
_onMotion(actor, event) {
const page = this._pageForCoords(...event.get_coords());
this._slideSidePages(page);
return Clutter.EVENT_PROPAGATE;
}
_onButtonPress(actor, event) {
const page = this._pageForCoords(...event.get_coords());
if (page === SidePages.NEXT)
@ -489,10 +652,6 @@ var BaseAppView = GObject.registerClass({
this.goToPage(this._grid.currentPage - 1);
}
_onLeave() {
this._slideSidePages(SidePages.NONE);
}
_swipeBegin(tracker, monitor) {
if (monitor !== Main.layoutManager.primaryIndex)
return;
@ -522,8 +681,6 @@ var BaseAppView = GObject.registerClass({
const adjustment = this._adjustment;
const value = endProgress * adjustment.page_size;
this._syncPageHints(endProgress);
adjustment.ease(value, {
mode: Clutter.AnimationMode.EASE_OUT_CUBIC,
duration,
@ -683,7 +840,7 @@ var BaseAppView = GObject.registerClass({
dragMotion: this._onDragMotion.bind(this),
};
DND.addDragMonitor(this._dragMonitor);
this._slideSidePages(SidePages.PREVIOUS | SidePages.NEXT | SidePages.DND);
this._appGridLayout.showPageIndicators();
this._dragFocus = null;
this._swipeTracker.enabled = false;
}
@ -694,14 +851,6 @@ var BaseAppView = GObject.registerClass({
const appIcon = dragEvent.source;
this._dropPage = this._pageForCoords(dragEvent.x, dragEvent.y);
if (this._dropPage &&
this._dropPage === SidePages.PREVIOUS &&
this._grid.currentPage === 0) {
delete this._dropPage;
return DND.DragMotionResult.NO_DROP;
}
// Handle the drag overshoot. When dragging to above the
// icon grid, move to the page above; when dragging below,
// move to the page below.
@ -720,8 +869,7 @@ var BaseAppView = GObject.registerClass({
}
this._resetOvershoot();
this._slideSidePages(SidePages.NONE);
delete this._dropPage;
this._appGridLayout.hidePageIndicators();
this._swipeTracker.enabled = true;
}
@ -729,7 +877,7 @@ var BaseAppView = GObject.registerClass({
// At this point, the positions aren't stored yet, thus _redisplay()
// will move all items to their original positions
this._redisplay();
this._slideSidePages(SidePages.NONE);
this._appGridLayout.hidePageIndicators();
this._swipeTracker.enabled = true;
}
@ -999,203 +1147,16 @@ var BaseAppView = GObject.registerClass({
this._grid.ease(params);
}
_syncPageHints(pageNumber, animate = true) {
const showingNextPage = this._pagesShown & SidePages.NEXT;
const showingPrevPage = this._pagesShown & SidePages.PREVIOUS;
const dnd = this._pagesShown & SidePages.DND;
const duration = animate ? PAGE_INDICATOR_FADE_TIME : 0;
if (showingPrevPage) {
const opacity = pageNumber === 0 ? 0 : 255;
this._prevPageIndicator.visible = true;
this._prevPageIndicator.ease({
opacity,
duration,
});
if (!dnd) {
this._prevPageArrow.visible = true;
this._prevPageArrow.ease({
opacity,
duration,
});
}
}
if (showingNextPage) {
const opacity = pageNumber === this._grid.nPages - 1 ? 0 : 255;
this._nextPageIndicator.visible = true;
this._nextPageIndicator.ease({
opacity,
duration,
});
if (!dnd) {
this._nextPageArrow.visible = true;
this._nextPageArrow.ease({
opacity,
duration,
});
}
}
}
goToPage(pageNumber, animate = true) {
pageNumber = Math.clamp(pageNumber, 0, this._grid.nPages - 1);
if (this._grid.currentPage === pageNumber)
return;
this._syncPageHints(pageNumber, animate);
this._appGridLayout.goToPage(pageNumber, animate);
this._grid.goToPage(pageNumber, animate);
}
_getIndicatorOffset(page, progress, baseOffset) {
const rtl = this.get_text_direction() === Clutter.TextDirection.RTL;
const translationX =
(1 - progress) * PAGE_PREVIEW_ANIMATION_START_OFFSET;
page = rtl ? -page : page;
return (translationX - baseOffset) * page;
}
_syncClip() {
const nextPageAdjustment = this._previewedPages.get(1);
const prevPageAdjustment = this._previewedPages.get(-1);
this._grid.clip_to_view =
(!prevPageAdjustment || prevPageAdjustment.value === 0) &&
(!nextPageAdjustment || nextPageAdjustment.value === 0);
}
_setupPagePreview(page, state) {
if (this._previewedPages.has(page))
return this._previewedPages.get(page);
const adjustment = new St.Adjustment({
actor: this,
lower: 0,
upper: 1,
});
const indicator = page > 0
? this._nextPageIndicator : this._prevPageIndicator;
adjustment.connectObject('notify::value', () => {
const nextPage = this._grid.currentPage + page;
const hasFollowingPage = nextPage >= 0 &&
nextPage < this._grid.nPages;
if (hasFollowingPage) {
const items = this._grid.getItemsAtPage(nextPage);
items.forEach(item => {
item.translation_x =
this._getIndicatorOffset(page, adjustment.value, 0);
});
if (!(state & SidePages.DND)) {
const pageArrow = page > 0
? this._nextPageArrow
: this._prevPageArrow;
pageArrow.set({
visible: true,
opacity: adjustment.value * 255,
translation_x: this._getIndicatorOffset(
page, adjustment.value,
this._pageArrowOffset),
});
}
}
if (hasFollowingPage ||
(page > 0 &&
this._grid.layout_manager.allow_incomplete_pages &&
(state & SidePages.DND) !== 0)) {
indicator.set({
visible: true,
opacity: adjustment.value * 255,
translation_x: this._getIndicatorOffset(
page, adjustment.value,
this._pageIndicatorOffset - indicator.width),
});
}
this._syncClip();
}, this);
this._previewedPages.set(page, adjustment);
return adjustment;
}
_teardownPagePreview(page) {
const adjustment = this._previewedPages.get(page);
if (!adjustment)
return;
adjustment.value = 1;
adjustment.disconnectObject(this);
this._previewedPages.delete(page);
}
_slideSidePages(state) {
if (this._pagesShown === state)
return;
this._pagesShown = state;
const showingNextPage = state & SidePages.NEXT;
const showingPrevPage = state & SidePages.PREVIOUS;
const dnd = state & SidePages.DND;
let adjustment;
if (dnd) {
this._nextPageIndicator.add_style_class_name('dnd');
this._prevPageIndicator.add_style_class_name('dnd');
} else {
this._nextPageIndicator.remove_style_class_name('dnd');
this._prevPageIndicator.remove_style_class_name('dnd');
}
adjustment = this._previewedPages.get(1);
if (showingNextPage) {
adjustment = this._setupPagePreview(1, state);
adjustment.ease(1, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
} else if (adjustment) {
adjustment.ease(0, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this._teardownPagePreview(1);
this._syncClip();
this._nextPageArrow.visible = false;
this._nextPageIndicator.visible = false;
},
});
}
adjustment = this._previewedPages.get(-1);
if (showingPrevPage) {
adjustment = this._setupPagePreview(-1, state);
adjustment.ease(1, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
});
} else if (adjustment) {
adjustment.ease(0, {
duration: PAGE_PREVIEW_ANIMATION_TIME,
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this._teardownPagePreview(-1);
this._syncClip();
this._prevPageArrow.visible = false;
this._prevPageIndicator.visible = false;
},
});
}
}
updateDragFocus(dragFocus) {
this._dragFocus = dragFocus;
}